diff --git a/http-inject-plugin/pom.xml b/http-inject-plugin/pom.xml
index 93c2b6966..329d0281e 100644
--- a/http-inject-plugin/pom.xml
+++ b/http-inject-plugin/pom.xml
@@ -19,7 +19,7 @@
io.avaje
- avaje-inject
+ avaje-inject-generator
12.0
provided
true
@@ -38,6 +38,28 @@
provided
true
+
+ io.javalin
+ javalin
+ 6.7.0
+ provided
+ true
+
+
+ io.avaje
+ avaje-jex
+ 3.3
+ provided
+ true
+
+
+ io.helidon.webserver
+ helidon-webserver
+ 4.3.2
+ provided
+ true
+
+
diff --git a/http-inject-plugin/src/main/java/io/avaje/http/inject/DefaultResolverProvider.java b/http-inject-plugin/src/main/java/io/avaje/http/inject/DefaultResolverProvider.java
index b22eb7714..e44ab9d84 100644
--- a/http-inject-plugin/src/main/java/io/avaje/http/inject/DefaultResolverProvider.java
+++ b/http-inject-plugin/src/main/java/io/avaje/http/inject/DefaultResolverProvider.java
@@ -4,10 +4,10 @@
import io.avaje.http.api.context.ThreadLocalRequestContextResolver;
import io.avaje.inject.BeanScopeBuilder;
import io.avaje.inject.spi.InjectPlugin;
-import io.avaje.spi.ServiceProvider;
+import io.avaje.inject.spi.PluginProvides;
/** Plugin for avaje inject that provides a default RequestContextResolver instance. */
-@ServiceProvider
+@PluginProvides
public final class DefaultResolverProvider implements InjectPlugin {
@Override
diff --git a/http-inject-plugin/src/main/java/io/avaje/http/inject/HelidonHandler.java b/http-inject-plugin/src/main/java/io/avaje/http/inject/HelidonHandler.java
new file mode 100644
index 000000000..c18f97b8f
--- /dev/null
+++ b/http-inject-plugin/src/main/java/io/avaje/http/inject/HelidonHandler.java
@@ -0,0 +1,32 @@
+package io.avaje.http.inject;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+import io.avaje.http.api.ValidationException;
+import io.helidon.common.Weight;
+import io.helidon.webserver.http.HttpFeature;
+import io.helidon.webserver.http.HttpRouting.Builder;
+import io.helidon.webserver.http.ServerRequest;
+import io.helidon.webserver.http.ServerResponse;
+
+@Weight(-67) // execute first so that it can be overridden by a custom error handler.
+final class HelidonHandler implements HttpFeature {
+
+ @Override
+ public void setup(Builder routing) {
+
+ routing.error(ValidationException.class, this::handle);
+ }
+
+ private void handle(ServerRequest req, ServerResponse res, ValidationException ex) {
+ try (var os =
+ res.status(ex.getStatus())
+ .header("Content-Type", "application/problem+json")
+ .outputStream()) {
+ new ValidationResponse(ex.getStatus(), ex.getErrors(), req.path().rawPath()).toJson(os);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/http-inject-plugin/src/main/java/io/avaje/http/inject/HttpValidatorErrorPlugin.java b/http-inject-plugin/src/main/java/io/avaje/http/inject/HttpValidatorErrorPlugin.java
new file mode 100644
index 000000000..627daad64
--- /dev/null
+++ b/http-inject-plugin/src/main/java/io/avaje/http/inject/HttpValidatorErrorPlugin.java
@@ -0,0 +1,56 @@
+package io.avaje.http.inject;
+
+import io.avaje.http.api.AvajeJavalinPlugin;
+import io.avaje.inject.BeanScopeBuilder;
+import io.avaje.inject.spi.InjectPlugin;
+import io.avaje.inject.spi.PluginProvides;
+import io.avaje.jex.Routing.HttpService;
+import io.helidon.webserver.http.HttpFeature;
+
+/** Plugin for avaje inject that provides a default Validator Handler */
+@PluginProvides(
+ providesStrings = {
+ "io.helidon.webserver.http.HttpFeature",
+ "io.avaje.http.api.AvajeJavalinPlugin",
+ "io.avaje.jex.Routing.HttpService",
+ })
+public final class HttpValidatorErrorPlugin implements InjectPlugin {
+
+ @Override
+ public void apply(BeanScopeBuilder builder) {
+
+ ModuleLayer bootLayer = ModuleLayer.boot();
+
+ bootLayer
+ .findModule("io.avaje.http.plugin")
+ .ifPresentOrElse(
+ m -> {
+ if (bootLayer.findModule("io.avaje.jex").isPresent()) {
+ builder.provideDefault(HttpService.class, JexHandler::new);
+ } else if (bootLayer.findModule("io.helidon.webserver").isPresent()) {
+ builder.provideDefault(HttpFeature.class, HelidonHandler::new);
+ } else if (bootLayer.findModule("io.javalin").isPresent()) {
+ builder.provideDefault(AvajeJavalinPlugin.class, JavalinHandler::new);
+ }
+ },
+ () -> {
+ try {
+ builder.provideDefault(HttpService.class, JexHandler::new);
+ return;
+ } catch (NoClassDefFoundError e) {
+ // not present
+ }
+ try {
+ builder.provideDefault(HttpFeature.class, HelidonHandler::new);
+ return;
+ } catch (NoClassDefFoundError e) {
+ // not present
+ }
+ try {
+ builder.provideDefault(AvajeJavalinPlugin.class, JavalinHandler::new);
+ } catch (NoClassDefFoundError e) {
+ // not present
+ }
+ });
+ }
+}
diff --git a/http-inject-plugin/src/main/java/io/avaje/http/inject/JavalinHandler.java b/http-inject-plugin/src/main/java/io/avaje/http/inject/JavalinHandler.java
new file mode 100644
index 000000000..84dc6ca8b
--- /dev/null
+++ b/http-inject-plugin/src/main/java/io/avaje/http/inject/JavalinHandler.java
@@ -0,0 +1,25 @@
+package io.avaje.http.inject;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+import io.avaje.http.api.AvajeJavalinPlugin;
+import io.avaje.http.api.ValidationException;
+import io.javalin.config.JavalinConfig;
+import io.javalin.http.Context;
+
+final class JavalinHandler extends AvajeJavalinPlugin {
+ @Override
+ public void onStart(JavalinConfig config) {
+ config.router.mount(r -> r.exception(ValidationException.class, this::handler));
+ }
+
+ private void handler(ValidationException ex, Context ctx) {
+ try (var os =
+ ctx.contentType("application/problem+json").status(ex.getStatus()).outputStream()) {
+ new ValidationResponse(ex.getStatus(), ex.getErrors(), ctx.path()).toJson(os);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/http-inject-plugin/src/main/java/io/avaje/http/inject/JexHandler.java b/http-inject-plugin/src/main/java/io/avaje/http/inject/JexHandler.java
new file mode 100644
index 000000000..a89678fad
--- /dev/null
+++ b/http-inject-plugin/src/main/java/io/avaje/http/inject/JexHandler.java
@@ -0,0 +1,27 @@
+package io.avaje.http.inject;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+
+import io.avaje.http.api.ValidationException;
+import io.avaje.jex.Routing;
+import io.avaje.jex.Routing.HttpService;
+import io.avaje.jex.http.Context;
+
+final class JexHandler implements HttpService {
+
+ @Override
+ public void add(Routing arg0) {
+ arg0.error(ValidationException.class, this::handler);
+ }
+
+ private void handler(Context ctx, ValidationException ex) {
+
+ try (var os =
+ ctx.contentType("application/problem+json").status(ex.getStatus()).outputStream()) {
+ new ValidationResponse(ex.getStatus(), ex.getErrors(), ctx.path()).toJson(os);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/http-inject-plugin/src/main/java/io/avaje/http/inject/ValidationResponse.java b/http-inject-plugin/src/main/java/io/avaje/http/inject/ValidationResponse.java
new file mode 100644
index 000000000..c0f4b50ea
--- /dev/null
+++ b/http-inject-plugin/src/main/java/io/avaje/http/inject/ValidationResponse.java
@@ -0,0 +1,120 @@
+package io.avaje.http.inject;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.List;
+
+import io.avaje.http.api.ValidationException.Violation;
+
+public class ValidationResponse {
+
+ private static String type = "tag:io.avaje.http.api.ValidationException";
+ private static String title = "Request Failed Validation";
+ private static String detail = "You tried to call this endpoint, but your data failed validation";
+ private final int status;
+ private final List errors;
+ private final String instance;
+
+ public ValidationResponse(int status, List errors, String instance) {
+ this.status = status;
+ this.errors = errors;
+ this.instance = instance;
+ }
+
+ // custom serialize as this is a simple class
+ public void toJson(OutputStream os) throws IOException {
+ try (Writer writer = new OutputStreamWriter(os, "UTF-8")) {
+ writeJsonInternal(writer);
+ }
+ }
+
+ private void writeJsonInternal(Writer writer) throws IOException {
+ writer.write('{');
+ writeKeyValue("type", type, writer);
+ writer.write(',');
+ writeKeyValue("title", title, writer);
+ writer.write(',');
+ writeKeyValue("detail", detail, writer);
+ writer.write(',');
+ writeKeyValue("instance", instance, writer);
+ writer.write(',');
+ // status is a number, so no quotes or escaping needed
+ writer.write("\"status\":");
+ writer.write(String.valueOf(status));
+ writer.write(",\"errors\":[");
+ for (int i = 0; i < errors.size(); i++) {
+ if (i > 0) {
+ writer.write(',');
+ }
+ var e = errors.get(i);
+ writer.write('{');
+ writeKeyValue("path", e.getPath(), writer);
+ writer.write(',');
+ writeKeyValue("field", e.getField(), writer);
+ writer.write(',');
+ writeKeyValue("message", e.getMessage(), writer);
+ writer.write('}');
+ }
+
+ writer.write(']');
+ writer.write('}');
+ writer.flush();
+ }
+
+ /** Writes a JSON key-value pair where the value is a string, handling quotes and escaping. */
+ private void writeKeyValue(String key, String value, Writer writer) throws IOException {
+ writer.write('"');
+ writer.write(key);
+ writer.write("\":");
+ writeEscapedJsonString(value, writer);
+ }
+
+ /**
+ * Writes the given string to the writer, JSON-escaping it and wrapping it in quotes. Writes
+ * 'null' (the JSON literal) if the input string is null.
+ */
+ private static void writeEscapedJsonString(String s, Writer writer) throws IOException {
+ if (s == null) {
+ writer.write("null");
+ return;
+ }
+
+ writer.write('"');
+ for (int i = 0; i < s.length(); i++) {
+ char ch = s.charAt(i);
+ switch (ch) {
+ case '"':
+ writer.write("\\\"");
+ break;
+ case '\\':
+ writer.write("\\\\");
+ break;
+ case '\b':
+ writer.write("\\b");
+ break;
+ case '\f':
+ writer.write("\\f");
+ break;
+ case '\n':
+ writer.write("\\n");
+ break;
+ case '\r':
+ writer.write("\\r");
+ break;
+ case '\t':
+ writer.write("\\t");
+ break;
+ default:
+ // Check for control characters that must be escaped
+ if (ch < ' ' || ch >= 0x7F && ch <= 0x9F) {
+ writer.write(String.format("\\u%04x", (int) ch));
+ } else {
+ writer.write(ch);
+ }
+ }
+ }
+ writer.write('"');
+ }
+}
diff --git a/http-inject-plugin/src/main/java/module-info.java b/http-inject-plugin/src/main/java/module-info.java
index e13c5e735..8c6058dfd 100644
--- a/http-inject-plugin/src/main/java/module-info.java
+++ b/http-inject-plugin/src/main/java/module-info.java
@@ -1,8 +1,12 @@
+import io.avaje.http.inject.DefaultResolverProvider;
+import io.avaje.http.inject.HttpValidatorErrorPlugin;
+
module io.avaje.http.plugin {
requires io.avaje.http.api;
requires io.avaje.inject;
- requires static io.avaje.spi;
-
- provides io.avaje.inject.spi.InjectExtension with io.avaje.http.inject.DefaultResolverProvider;
+ requires static io.avaje.jex;
+ requires static io.javalin;
+ requires static io.helidon.webserver;
+ provides io.avaje.inject.spi.InjectExtension with DefaultResolverProvider, HttpValidatorErrorPlugin;
}
diff --git a/pom.xml b/pom.xml
index f426ba264..ed05fd10b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -44,7 +44,6 @@
http-client
http-client-gson-adapter
http-client-moshi-adapter
- http-inject-plugin
http-generator-core
http-generator-javalin
http-generator-sigma
@@ -68,6 +67,7 @@
[21,)
+ http-inject-plugin
htmx-nima
htmx-nima-jstache
http-generator-helidon