diff --git a/code/common/service/java/nu/marginalia/service/server/JoobyService.java b/code/common/service/java/nu/marginalia/service/server/JoobyService.java index 25f9ed52..8c5eb9d8 100644 --- a/code/common/service/java/nu/marginalia/service/server/JoobyService.java +++ b/code/common/service/java/nu/marginalia/service/server/JoobyService.java @@ -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()); diff --git a/code/services-core/assistant-service/build.gradle b/code/services-core/assistant-service/build.gradle index 2328604d..0b90263f 100644 --- a/code/services-core/assistant-service/build.gradle +++ b/code/services-core/assistant-service/build.gradle @@ -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 diff --git a/code/services-core/assistant-service/java/nu/marginalia/assistant/AssistantMain.java b/code/services-core/assistant-service/java/nu/marginalia/assistant/AssistantMain.java index ddafcad2..63d605bd 100644 --- a/code/services-core/assistant-service/java/nu/marginalia/assistant/AssistantMain.java +++ b/code/services-core/assistant-service/java/nu/marginalia/assistant/AssistantMain.java @@ -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); } } diff --git a/code/services-core/assistant-service/java/nu/marginalia/assistant/AssistantService.java b/code/services-core/assistant-service/java/nu/marginalia/assistant/AssistantService.java index 4271d7c2..7dede0cd 100644 --- a/code/services-core/assistant-service/java/nu/marginalia/assistant/AssistantService.java +++ b/code/services-core/assistant-service/java/nu/marginalia/assistant/AssistantService.java @@ -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())); } } diff --git a/code/services-core/assistant-service/java/nu/marginalia/assistant/ScreenshotService.java b/code/services-core/assistant-service/java/nu/marginalia/assistant/ScreenshotService.java new file mode 100644 index 00000000..058ffe43 --- /dev/null +++ b/code/services-core/assistant-service/java/nu/marginalia/assistant/ScreenshotService.java @@ -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 """ + + + + + Placeholder + %s + + + """.formatted(name); + } + +}