(assistant) Migrate to Jooby framework

This commit is contained in:
Viktor Lofgren 2025-02-15 13:21:48 +01:00
parent f076d05595
commit f553701224
5 changed files with 160 additions and 32 deletions

View File

@ -15,6 +15,7 @@ import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
@ -106,9 +107,12 @@ public class JoobyService {
config.externalAddress());
// FIXME: This won't work outside of docker, may need to submit a PR to jooby to allow classpaths here
jooby.install(new JteModule(Path.of("/app/resources/jte"), Path.of("/app/classes/jte-precompiled")));
jooby.assets("/*", Paths.get("/app/resources/static"));
if (Files.exists(Path.of("/app/resources/jte")) || Files.exists(Path.of("/app/classes/jte-precompiled"))) {
jooby.install(new JteModule(Path.of("/app/resources/jte"), Path.of("/app/classes/jte-precompiled")));
}
if (Files.exists(Path.of("/app/resources/static"))) {
jooby.assets("/*", Paths.get("/app/resources/static"));
}
var options = new ServerOptions();
options.setHost(config.bindAddress());
options.setPort(restEndpoint.port());

View File

@ -37,8 +37,6 @@ dependencies {
implementation project(':code:functions:domain-info')
implementation project(':code:functions:domain-info:api')
implementation project(':code:features-search:screenshots')
implementation project(':code:libraries:geo-ip')
implementation project(':code:libraries:language-processing')
implementation project(':code:libraries:term-frequency-dict')
@ -48,6 +46,7 @@ dependencies {
implementation libs.bundles.slf4j
implementation libs.prometheus
implementation libs.commons.io
implementation libs.guava
libs.bundles.grpc.get().each {
implementation dependencies.create(it) {
@ -61,9 +60,7 @@ dependencies {
implementation dependencies.create(libs.guice.get()) {
exclude group: 'com.google.guava'
}
implementation dependencies.create(libs.spark.get()) {
exclude group: 'org.eclipse.jetty'
}
implementation libs.bundles.jooby
implementation libs.bundles.jetty
implementation libs.opencsv
implementation libs.trove

View File

@ -3,6 +3,8 @@ package nu.marginalia.assistant;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import io.jooby.ExecutionMode;
import io.jooby.Jooby;
import nu.marginalia.livecapture.LivecaptureModule;
import nu.marginalia.service.MainClass;
import nu.marginalia.service.ServiceId;
@ -38,8 +40,17 @@ public class AssistantMain extends MainClass {
var configuration = injector.getInstance(ServiceConfiguration.class);
orchestrateBoot(registry, configuration);
injector.getInstance(AssistantMain.class);
var main = injector.getInstance(AssistantMain.class);
injector.getInstance(Initialization.class).setReady();
Jooby.runApp(new String[] { "application.env=prod" }, ExecutionMode.WORKER, () -> new Jooby() {
{
main.start(this);
}
});
}
public void start(Jooby jooby) {
service.startJooby(jooby);
}
}

View File

@ -2,27 +2,27 @@ package nu.marginalia.assistant;
import com.google.gson.Gson;
import com.google.inject.Inject;
import io.jooby.Context;
import io.jooby.Jooby;
import nu.marginalia.assistant.suggest.Suggestions;
import nu.marginalia.functions.domains.DomainInfoGrpcService;
import nu.marginalia.functions.math.MathGrpcService;
import nu.marginalia.livecapture.LiveCaptureGrpcService;
import nu.marginalia.model.gson.GsonFactory;
import nu.marginalia.rss.svc.FeedsGrpcService;
import nu.marginalia.screenshot.ScreenshotService;
import nu.marginalia.service.discovery.property.ServicePartition;
import nu.marginalia.service.server.BaseServiceParams;
import nu.marginalia.service.server.SparkService;
import nu.marginalia.service.server.JoobyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import spark.Spark;
import java.util.List;
public class AssistantService extends SparkService {
public class AssistantService extends JoobyService {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final Gson gson = GsonFactory.get();
@org.jetbrains.annotations.NotNull
private final ScreenshotService screenshotService;
private final Suggestions suggestions;
@Inject
@ -39,30 +39,30 @@ public class AssistantService extends SparkService {
List.of(domainInfoGrpcService,
mathGrpcService,
liveCaptureGrpcService,
feedsGrpcService));
feedsGrpcService),
List.of());
this.screenshotService = screenshotService;
this.suggestions = suggestions;
Spark.staticFiles.expireTime(600);
Spark.get("/screenshot/:id", screenshotService::serveScreenshotRequest);
Spark.get("/suggest/", this::getSuggestions, this::convertToJson);
Spark.awaitInitialization();
}
private Object getSuggestions(Request request, Response response) {
response.type("application/json");
var param = request.queryParams("partial");
if (param == null) {
public void startJooby(Jooby jooby) {
super.startJooby(jooby);
jooby.get("/suggest", this::getSuggestions);
jooby.get("/screenshot/{id}", screenshotService::serveScreenshotRequest);
}
private String getSuggestions(Context context) {
context.setResponseType("application/json");
var param = context.query("partial");
if (param.isMissing()) {
logger.warn("Bad parameter, partial is null");
Spark.halt(500);
context.setResponseCode(500);
return "{}";
}
return suggestions.getSuggestions(10, param);
}
private String convertToJson(Object o) {
return gson.toJson(o);
return gson.toJson(suggestions.getSuggestions(10, param.value()));
}
}

View File

@ -0,0 +1,116 @@
package nu.marginalia.assistant;
import com.google.common.base.Strings;
import com.google.inject.Inject;
import com.zaxxer.hikari.HikariDataSource;
import io.jooby.Context;
import nu.marginalia.db.DbDomainQueries;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.sql.SQLException;
public class ScreenshotService {
private final DbDomainQueries domainQueries;
private final HikariDataSource dataSource;
private final Logger logger = LoggerFactory.getLogger(getClass());
@Inject
public ScreenshotService(DbDomainQueries dbDomainQueries, HikariDataSource dataSource) {
this.domainQueries = dbDomainQueries;
this.dataSource = dataSource;
}
public boolean hasScreenshot(int domainId) {
try (var conn = dataSource.getConnection();
var ps = conn.prepareStatement("""
SELECT TRUE
FROM DATA_DOMAIN_SCREENSHOT
INNER JOIN EC_DOMAIN ON EC_DOMAIN.DOMAIN_NAME=DATA_DOMAIN_SCREENSHOT.DOMAIN_NAME
WHERE EC_DOMAIN.ID=?
""")) {
ps.setInt(1, domainId);
var rs = ps.executeQuery();
if (rs.next()) {
return rs.getBoolean(1);
}
}
catch (SQLException ex) {
logger.warn("SQL error", ex);
}
return false;
}
public Object serveScreenshotRequest(Context context) {
if (Strings.isNullOrEmpty(context.path("id").value(""))) {
context.setResponseCode(404);
return "";
}
int id = context.path("id").intValue();
try (var conn = dataSource.getConnection();
var ps = conn.prepareStatement("""
SELECT CONTENT_TYPE, DATA
FROM DATA_DOMAIN_SCREENSHOT
INNER JOIN EC_DOMAIN ON EC_DOMAIN.DOMAIN_NAME=DATA_DOMAIN_SCREENSHOT.DOMAIN_NAME
WHERE EC_DOMAIN.ID=?
""")) {
ps.setInt(1, id);
var rsp = ps.executeQuery();
if (rsp.next()) {
context.setResponseType(rsp.getString(1));
context.setResponseCode(200);
context.setResponseHeader("Cache-control", "public,max-age=3600");
IOUtils.copy(rsp.getBlob(2).getBinaryStream(), context.responseStream());
return "";
}
}
catch (IOException ex) {
logger.warn("IO error", ex);
}
catch (SQLException ex) {
logger.warn("SQL error", ex);
}
context.setResponseType("image/svg+xml");
var name = domainQueries.getDomain(id).map(Object::toString)
.orElse("[Screenshot Not Yet Captured]");
return """
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
width="640px"
height="480px"
viewBox="0 0 640 480"
version="1.1">
<g>
<rect
style="fill:#808080"
id="rect288"
width="595.41992"
height="430.01825"
x="23.034981"
y="27.850344" />
<text
xml:space="preserve"
style="font-size:100px;fill:#909090;font-family:sans-serif;"
x="20"
y="120">Placeholder</text>
<text
xml:space="preserve"
style="font-size:32px;fill:#000000;font-family:monospace;"
x="320" y="240" dominant-baseline="middle" text-anchor="middle">%s</text>
</g>
</svg>
""".formatted(name);
}
}