Skip to content
Open
22 changes: 22 additions & 0 deletions http-inject-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>6.7.0</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.avaje</groupId>
<artifactId>avaje-jex</artifactId>
<version>3.3</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.helidon.webserver</groupId>
<artifactId>helidon-webserver</artifactId>
<version>4.3.2</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>

</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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.avaje.spi.ServiceProvider;
import io.helidon.webserver.http.HttpFeature;

/** Plugin for avaje inject that provides a default Validator Handler */
@ServiceProvider
@PluginProvides(
providesStrings = {
"io.helidon.webserver.http.HttpFeature",
"io.avaje.http.api.AvajeJavalinPlugin",
"io.avaje.jex.Routing.HttpService",
})
public final class HttpValidatorHandler implements InjectPlugin {

enum Server {
HELIDON("io.helidon.webserver.http.HttpFeature"),
JAVALIN("io.javalin.plugin.Plugin"),
JEX("io.avaje.jex.Routing.HttpService");
String register;
Server(String register) {
this.register = register;
}
}

private static final Server type = server();

private static Server server() {
for (var register : Server.values()) {
try {
Class.forName(register.register);
return register;
} catch (ClassNotFoundException e) {
// nothing
}
}
return null;
}

@Override
public void apply(BeanScopeBuilder builder) {
if (type == null) {
return;
}
switch (type) {
case HELIDON:
builder.provideDefault(HttpFeature.class, HelidonHandler::new);
break;
case JAVALIN:
builder.provideDefault(AvajeJavalinPlugin.class, JavalinHandler::new);
break;
case JEX:
builder.provideDefault(HttpService.class, JexHandler::new);
break;
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Violation> errors;
private final String instance;

public ValidationResponse(int status, List<Violation> 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('"');
}
}
9 changes: 7 additions & 2 deletions http-inject-plugin/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import io.avaje.http.inject.DefaultResolverProvider;
import io.avaje.http.inject.HttpValidatorHandler;

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, HttpValidatorHandler;
}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
<module>http-client</module>
<module>http-client-gson-adapter</module>
<module>http-client-moshi-adapter</module>
<module>http-inject-plugin</module>
<module>http-generator-core</module>
<module>http-generator-javalin</module>
<module>http-generator-sigma</module>
Expand All @@ -68,6 +67,7 @@
<jdk>[21,)</jdk>
</activation>
<modules>
<module>http-inject-plugin</module>
<module>htmx-nima</module>
<module>htmx-nima-jstache</module>
<module>http-generator-helidon</module>
Expand Down