(search) Remove Spark and migrate to Jooby for the search service

This commit is contained in:
Viktor Lofgren 2024-12-10 19:13:13 +01:00
parent 2fbf201761
commit fdee07048d
60 changed files with 731 additions and 3101 deletions

View File

@ -48,6 +48,7 @@ ext {
dockerImageTag='latest' dockerImageTag='latest'
dockerImageRegistry='marginalia' dockerImageRegistry='marginalia'
jibVersion = '3.4.3' jibVersion = '3.4.3'
} }
idea { idea {

View File

@ -42,6 +42,12 @@ dependencies {
implementation libs.bundles.curator implementation libs.bundles.curator
implementation libs.bundles.flyway implementation libs.bundles.flyway
libs.bundles.jooby.get().each {
implementation dependencies.create(it) {
exclude group: 'org.slf4j'
}
}
testImplementation libs.bundles.slf4j.test testImplementation libs.bundles.slf4j.test
implementation libs.bundles.mariadb implementation libs.bundles.mariadb

View File

@ -0,0 +1,174 @@
package nu.marginalia.service.server;
import io.jooby.*;
import io.prometheus.client.Counter;
import nu.marginalia.mq.inbox.MqInboxIf;
import nu.marginalia.service.client.ServiceNotAvailableException;
import nu.marginalia.service.discovery.property.ServiceEndpoint;
import nu.marginalia.service.discovery.property.ServiceKey;
import nu.marginalia.service.discovery.property.ServicePartition;
import nu.marginalia.service.module.ServiceConfiguration;
import nu.marginalia.service.server.jte.JteModule;
import nu.marginalia.service.server.mq.ServiceMqSubscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.List;
public class JoobyService {
private final Logger logger = LoggerFactory.getLogger(getClass());
// Marker for filtering out sensitive content from the persistent logs
private final Marker httpMarker = MarkerFactory.getMarker("HTTP");
private final Initialization initialization;
private final static Counter request_counter = Counter.build("wmsa_request_counter", "Request Counter")
.labelNames("service", "node")
.register();
private final static Counter request_counter_good = Counter.build("wmsa_request_counter_good", "Good Requests")
.labelNames("service", "node")
.register();
private final static Counter request_counter_bad = Counter.build("wmsa_request_counter_bad", "Bad Requests")
.labelNames("service", "node")
.register();
private final static Counter request_counter_err = Counter.build("wmsa_request_counter_err", "Error Requests")
.labelNames("service", "node")
.register();
private final String serviceName;
private static volatile boolean initialized = false;
protected final MqInboxIf messageQueueInbox;
private final int node;
private GrpcServer grpcServer;
private ServiceConfiguration config;
private final List<MvcExtension> joobyServices;
private final ServiceEndpoint restEndpoint;
public JoobyService(BaseServiceParams params,
ServicePartition partition,
List<DiscoverableService> grpcServices,
List<MvcExtension> joobyServices
) throws Exception {
this.joobyServices = joobyServices;
this.initialization = params.initialization;
config = params.configuration;
node = config.node();
String inboxName = config.serviceName();
logger.info("Inbox name: {}", inboxName);
var serviceRegistry = params.serviceRegistry;
restEndpoint = serviceRegistry.registerService(ServiceKey.forRest(config.serviceId(), config.node()),
config.instanceUuid(), config.externalAddress());
var mqInboxFactory = params.messageQueueInboxFactory;
messageQueueInbox = mqInboxFactory.createSynchronousInbox(inboxName, config.node(), config.instanceUuid());
messageQueueInbox.subscribe(new ServiceMqSubscription(this));
serviceName = System.getProperty("service-name");
initialization.addCallback(params.heartbeat::start);
initialization.addCallback(messageQueueInbox::start);
initialization.addCallback(() -> params.eventLog.logEvent("SVC-INIT", serviceName + ":" + config.node()));
initialization.addCallback(() -> serviceRegistry.announceInstance(config.instanceUuid()));
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
if (e instanceof ServiceNotAvailableException) {
// reduce log spam for this common case
logger.error("Service not available: {}", e.getMessage());
}
else {
logger.error("Uncaught exception", e);
}
request_counter_err.labels(serviceName, Integer.toString(node)).inc();
});
if (!initialization.isReady() && ! initialized ) {
initialized = true;
grpcServer = new GrpcServer(config, serviceRegistry, partition, grpcServices);
grpcServer.start();
}
}
public void startJooby(Jooby jooby) {
logger.info("{} Listening to {}:{} ({})", getClass().getSimpleName(),
restEndpoint.host(),
restEndpoint.port(),
config.externalAddress());
jooby.install(new JteModule(Path.of("/app/resources/jte"), Path.of("/app/classes/jte-precompiled")));
var options = new ServerOptions();
options.setHost(config.bindAddress());
options.setPort(restEndpoint.port());
jooby.setServerOptions(options);
jooby.get("/internal/ping", ctx -> "pong");
jooby.get("/internal/started", this::isInitialized);
jooby.get("/internal/ready", this::isReady);
for (var service : joobyServices) {
jooby.mvc(service);
}
jooby.assets("/webfonts/*", Paths.get("/app/resources/static/webfonts"))
.setMaxAge(Duration.ofDays(365));
jooby.assets("/*", Paths.get("/app/resources/static"));
jooby.before(this::auditRequestIn);
jooby.after(this::auditRequestOut);
}
private Object isInitialized(Context ctx) {
if (initialization.isReady()) {
return "ok";
}
else {
ctx.setResponseCode(StatusCode.FAILED_DEPENDENCY_CODE);
return "bad";
}
}
public boolean isReady() {
return true;
}
private String isReady(Context ctx) {
if (isReady()) {
return "ok";
}
else {
ctx.setResponseCode(StatusCode.FAILED_DEPENDENCY_CODE);
return "bad";
}
}
private void auditRequestIn(Context ctx) {
request_counter.labels(serviceName, Integer.toString(node)).inc();
}
private void auditRequestOut(Context ctx, Object result, Throwable failure) {
if (ctx.getResponseCode().value() < 400) {
request_counter_good.labels(serviceName, Integer.toString(node)).inc();
}
else {
request_counter_bad.labels(serviceName, Integer.toString(node)).inc();
}
if (failure != null) {
logger.error("Request failed " + ctx.getMethod() + " " + ctx.getRequestURL(), failure);
request_counter_err.labels(serviceName, Integer.toString(node)).inc();
}
}
}

View File

@ -16,7 +16,7 @@ import spark.Spark;
import java.util.List; import java.util.List;
public class Service { public class SparkService {
private final Logger logger = LoggerFactory.getLogger(getClass()); private final Logger logger = LoggerFactory.getLogger(getClass());
// Marker for filtering out sensitive content from the persistent logs // Marker for filtering out sensitive content from the persistent logs
@ -43,10 +43,10 @@ public class Service {
private final int node; private final int node;
private GrpcServer grpcServer; private GrpcServer grpcServer;
public Service(BaseServiceParams params, public SparkService(BaseServiceParams params,
Runnable configureStaticFiles, Runnable configureStaticFiles,
ServicePartition partition, ServicePartition partition,
List<DiscoverableService> grpcServices) throws Exception { List<DiscoverableService> grpcServices) throws Exception {
this.initialization = params.initialization; this.initialization = params.initialization;
var config = params.configuration; var config = params.configuration;
@ -126,18 +126,18 @@ public class Service {
} }
} }
public Service(BaseServiceParams params, public SparkService(BaseServiceParams params,
ServicePartition partition, ServicePartition partition,
List<DiscoverableService> grpcServices) throws Exception { List<DiscoverableService> grpcServices) throws Exception {
this(params, this(params,
Service::defaultSparkConfig, SparkService::defaultSparkConfig,
partition, partition,
grpcServices); grpcServices);
} }
public Service(BaseServiceParams params) throws Exception { public SparkService(BaseServiceParams params) throws Exception {
this(params, this(params,
Service::defaultSparkConfig, SparkService::defaultSparkConfig,
ServicePartition.any(), ServicePartition.any(),
List.of()); List.of());
} }

View File

@ -0,0 +1,61 @@
package nu.marginalia.service.server.jte;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import gg.jte.ContentType;
import gg.jte.TemplateEngine;
import gg.jte.resolve.DirectoryCodeResolver;
import io.jooby.*;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
// Temporary workaround for a bug
// APL-2.0 https://github.com/jooby-project/jooby
public class JteModule implements Extension {
private Path sourceDirectory;
private Path classDirectory;
private TemplateEngine templateEngine;
public JteModule(@NonNull Path sourceDirectory, @NonNull Path classDirectory) {
this.sourceDirectory = (Path)Objects.requireNonNull(sourceDirectory, "Source directory is required.");
this.classDirectory = (Path)Objects.requireNonNull(classDirectory, "Class directory is required.");
}
public JteModule(@NonNull Path sourceDirectory) {
this.sourceDirectory = (Path)Objects.requireNonNull(sourceDirectory, "Source directory is required.");
}
public JteModule(@NonNull TemplateEngine templateEngine) {
this.templateEngine = (TemplateEngine)Objects.requireNonNull(templateEngine, "Template engine is required.");
}
public void install(@NonNull Jooby application) {
if (this.templateEngine == null) {
this.templateEngine = create(application.getEnvironment(), this.sourceDirectory, this.classDirectory);
}
ServiceRegistry services = application.getServices();
services.put(TemplateEngine.class, this.templateEngine);
application.encoder(MediaType.html, new JteTemplateEngine(this.templateEngine));
}
public static TemplateEngine create(@NonNull Environment environment, @NonNull Path sourceDirectory, @Nullable Path classDirectory) {
boolean dev = environment.isActive("dev", new String[]{"test"});
if (dev) {
Objects.requireNonNull(sourceDirectory, "Source directory is required.");
Path requiredClassDirectory = (Path)Optional.ofNullable(classDirectory).orElseGet(() -> sourceDirectory.resolve("jte-classes"));
TemplateEngine engine = TemplateEngine.create(new DirectoryCodeResolver(sourceDirectory), requiredClassDirectory, ContentType.Html, environment.getClassLoader());
Optional<List<String>> var10000 = Optional.ofNullable(System.getProperty("jooby.run.classpath")).map((it) -> it.split(File.pathSeparator)).map(Stream::of).map(Stream::toList);
Objects.requireNonNull(engine);
var10000.ifPresent(engine::setClassPath);
return engine;
} else {
return classDirectory == null ? TemplateEngine.createPrecompiled(ContentType.Html) : TemplateEngine.createPrecompiled(classDirectory, ContentType.Html);
}
}
}

View File

@ -0,0 +1,48 @@
package nu.marginalia.service.server.jte;
import edu.umd.cs.findbugs.annotations.NonNull;
import gg.jte.TemplateEngine;
import io.jooby.Context;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import io.jooby.buffer.DataBuffer;
import io.jooby.internal.jte.DataBufferOutput;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
// Temporary workaround for a bug
// APL-2.0 https://github.com/jooby-project/jooby
class JteTemplateEngine implements io.jooby.TemplateEngine {
private final TemplateEngine jte;
private final List<String> extensions;
public JteTemplateEngine(TemplateEngine jte) {
this.jte = jte;
this.extensions = List.of(".jte", ".kte");
}
@NonNull @Override
public List<String> extensions() {
return extensions;
}
@Override
public DataBuffer render(Context ctx, ModelAndView modelAndView) {
var buffer = ctx.getBufferFactory().allocateBuffer();
var output = new DataBufferOutput(buffer, StandardCharsets.UTF_8);
var attributes = ctx.getAttributes();
if (modelAndView instanceof MapModelAndView mapModelAndView) {
var mapModel = new HashMap<String, Object>();
mapModel.putAll(attributes);
mapModel.putAll(mapModelAndView.getModel());
jte.render(modelAndView.getView(), mapModel, output);
} else {
jte.render(modelAndView.getView(), modelAndView.getModel(), output);
}
return buffer;
}
}

View File

@ -3,7 +3,6 @@ package nu.marginalia.service.server.mq;
import nu.marginalia.mq.MqMessage; import nu.marginalia.mq.MqMessage;
import nu.marginalia.mq.inbox.MqInboxResponse; import nu.marginalia.mq.inbox.MqInboxResponse;
import nu.marginalia.mq.inbox.MqSubscription; import nu.marginalia.mq.inbox.MqSubscription;
import nu.marginalia.service.server.Service;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -15,10 +14,10 @@ import java.util.Map;
public class ServiceMqSubscription implements MqSubscription { public class ServiceMqSubscription implements MqSubscription {
private static final Logger logger = LoggerFactory.getLogger(ServiceMqSubscription.class); private static final Logger logger = LoggerFactory.getLogger(ServiceMqSubscription.class);
private final Map<String, Method> requests = new HashMap<>(); private final Map<String, Method> requests = new HashMap<>();
private final Service service; private final Object service;
public ServiceMqSubscription(Service service) { public ServiceMqSubscription(Object service) {
this.service = service; this.service = service;
/* Wire up all methods annotated with @MqRequest and @MqNotification /* Wire up all methods annotated with @MqRequest and @MqNotification

View File

@ -11,7 +11,7 @@ import nu.marginalia.api.svc.RateLimiterService;
import nu.marginalia.api.svc.ResponseCache; import nu.marginalia.api.svc.ResponseCache;
import nu.marginalia.model.gson.GsonFactory; import nu.marginalia.model.gson.GsonFactory;
import nu.marginalia.service.server.BaseServiceParams; import nu.marginalia.service.server.BaseServiceParams;
import nu.marginalia.service.server.Service; import nu.marginalia.service.server.SparkService;
import nu.marginalia.service.server.mq.MqRequest; import nu.marginalia.service.server.mq.MqRequest;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -21,7 +21,7 @@ import spark.Request;
import spark.Response; import spark.Response;
import spark.Spark; import spark.Spark;
public class ApiService extends Service { public class ApiService extends SparkService {
private final Logger logger = LoggerFactory.getLogger(getClass()); private final Logger logger = LoggerFactory.getLogger(getClass());
private final Gson gson = GsonFactory.get(); private final Gson gson = GsonFactory.get();

View File

@ -9,7 +9,7 @@ import nu.marginalia.renderer.MustacheRenderer;
import nu.marginalia.renderer.RendererFactory; import nu.marginalia.renderer.RendererFactory;
import nu.marginalia.screenshot.ScreenshotService; import nu.marginalia.screenshot.ScreenshotService;
import nu.marginalia.service.server.BaseServiceParams; import nu.marginalia.service.server.BaseServiceParams;
import nu.marginalia.service.server.Service; import nu.marginalia.service.server.SparkService;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import spark.Request; import spark.Request;
import spark.Response; import spark.Response;
@ -18,7 +18,7 @@ import spark.Spark;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
public class DatingService extends Service { public class DatingService extends SparkService {
private final DomainBlacklist blacklist; private final DomainBlacklist blacklist;
private final DbBrowseDomainsSimilarCosine browseSimilarCosine; private final DbBrowseDomainsSimilarCosine browseSimilarCosine;
private final DbBrowseDomainsRandom browseRandom; private final DbBrowseDomainsRandom browseRandom;

View File

@ -5,7 +5,7 @@ import com.zaxxer.hikari.HikariDataSource;
import nu.marginalia.renderer.MustacheRenderer; import nu.marginalia.renderer.MustacheRenderer;
import nu.marginalia.renderer.RendererFactory; import nu.marginalia.renderer.RendererFactory;
import nu.marginalia.service.server.BaseServiceParams; import nu.marginalia.service.server.BaseServiceParams;
import nu.marginalia.service.server.Service; import nu.marginalia.service.server.SparkService;
import nu.marginalia.service.server.StaticResources; import nu.marginalia.service.server.StaticResources;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import spark.Request; import spark.Request;
@ -15,7 +15,7 @@ import spark.Spark;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.*; import java.util.*;
public class ExplorerService extends Service { public class ExplorerService extends SparkService {
private final MustacheRenderer<Object> renderer; private final MustacheRenderer<Object> renderer;
private final HikariDataSource dataSource; private final HikariDataSource dataSource;

View File

@ -1,10 +1,8 @@
plugins { plugins {
id 'java' id 'java'
id 'io.freefair.sass-base' version '8.4'
id 'io.freefair.sass-java' version '8.4'
id 'application' id 'application'
id 'jvm-test-suite' id 'jvm-test-suite'
id 'gg.jte.gradle' version '3.1.15'
id 'com.google.cloud.tools.jib' version '3.4.3' id 'com.google.cloud.tools.jib' version '3.4.3'
} }
@ -15,18 +13,16 @@ application {
tasks.distZip.enabled = false tasks.distZip.enabled = false
jte {
sourceDirectory = java.nio.file.Path.of('resources/jte')
targetDirectory = java.nio.file.Path.of('build/classes/jte-precompiled')
generate()
}
java { java {
toolchain { toolchain {
languageVersion.set(JavaLanguageVersion.of(rootProject.ext.jvmVersion)) languageVersion.set(JavaLanguageVersion.of(rootProject.ext.jvmVersion))
} }
} }
sass {
sourceMapEnabled = true
sourceMapEmbed = true
outputStyle = EXPANDED
}
apply from: "$rootProject.projectDir/srcsets.gradle" apply from: "$rootProject.projectDir/srcsets.gradle"
apply from: "$rootProject.projectDir/docker.gradle" apply from: "$rootProject.projectDir/docker.gradle"
@ -64,18 +60,24 @@ dependencies {
exclude group: 'com.google.guava' exclude group: 'com.google.guava'
} }
implementation libs.handlebars implementation libs.handlebars
implementation dependencies.create(libs.spark.get()) {
exclude group: 'org.eclipse.jetty'
}
implementation libs.bundles.jetty
implementation libs.opencsv implementation libs.opencsv
implementation libs.trove implementation libs.trove
implementation libs.jte implementation libs.jte
libs.bundles.jooby.get().each {
implementation dependencies.create(it) {
// Jooby pulls in an incompatible slf4j version that breaks all logs
exclude group: 'org.slf4j'
}
}
implementation libs.fastutil implementation libs.fastutil
implementation libs.bundles.gson implementation libs.bundles.gson
implementation libs.bundles.mariadb implementation libs.bundles.mariadb
implementation libs.bundles.nlp implementation libs.bundles.nlp
annotationProcessor libs.jooby.apt
testImplementation libs.bundles.slf4j.test testImplementation libs.bundles.slf4j.test
testImplementation libs.bundles.junit testImplementation libs.bundles.junit
testImplementation libs.mockito testImplementation libs.mockito
@ -85,12 +87,14 @@ dependencies {
testImplementation 'org.testcontainers:mariadb:1.17.4' testImplementation 'org.testcontainers:mariadb:1.17.4'
testImplementation 'org.testcontainers:junit-jupiter:1.17.4' testImplementation 'org.testcontainers:junit-jupiter:1.17.4'
testImplementation project(':code:libraries:test-helpers') testImplementation project(':code:libraries:test-helpers')
testImplementation dependencies.create(libs.spark.get())
} }
task compileTailwind { task compileTailwind {
def inputFile = file('tailwind/globals.css') def inputFile = file('tailwind/globals.css')
def configFile = file('tailwind/tailwind.config.js') def configFile = file('tailwind/tailwind.config.js')
def outputFile = file('resources/static/search/css/style.css') def outputFile = file('resources/static/css/style.css')
def jteDir = file('resources/jte') def jteDir = file('resources/jte')
inputs.file inputFile inputs.file inputFile

View File

@ -3,15 +3,16 @@ package nu.marginalia.search;
import com.google.inject.Guice; import com.google.inject.Guice;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Injector; import com.google.inject.Injector;
import io.jooby.ExecutionMode;
import io.jooby.Jooby;
import nu.marginalia.service.MainClass; import nu.marginalia.service.MainClass;
import nu.marginalia.service.discovery.ServiceRegistryIf;
import nu.marginalia.service.module.ServiceConfiguration;
import nu.marginalia.service.module.ServiceDiscoveryModule;
import nu.marginalia.service.ServiceId; import nu.marginalia.service.ServiceId;
import nu.marginalia.service.module.ServiceConfigurationModule; import nu.marginalia.service.discovery.ServiceRegistryIf;
import nu.marginalia.service.module.DatabaseModule; import nu.marginalia.service.module.DatabaseModule;
import nu.marginalia.service.module.ServiceConfiguration;
import nu.marginalia.service.module.ServiceConfigurationModule;
import nu.marginalia.service.module.ServiceDiscoveryModule;
import nu.marginalia.service.server.Initialization; import nu.marginalia.service.server.Initialization;
import spark.Spark;
public class SearchMain extends MainClass { public class SearchMain extends MainClass {
private final SearchService service; private final SearchService service;
@ -25,8 +26,6 @@ public class SearchMain extends MainClass {
init(ServiceId.Search, args); init(ServiceId.Search, args);
Spark.staticFileLocation("/static/search/");
Injector injector = Guice.createInjector( Injector injector = Guice.createInjector(
new SearchModule(), new SearchModule(),
new ServiceConfigurationModule(ServiceId.Search), new ServiceConfigurationModule(ServiceId.Search),
@ -34,14 +33,22 @@ public class SearchMain extends MainClass {
new DatabaseModule(false) new DatabaseModule(false)
); );
// Orchestrate the boot order for the services // Orchestrate the boot order for the services
var registry = injector.getInstance(ServiceRegistryIf.class); var registry = injector.getInstance(ServiceRegistryIf.class);
var configuration = injector.getInstance(ServiceConfiguration.class); var configuration = injector.getInstance(ServiceConfiguration.class);
orchestrateBoot(registry, configuration); orchestrateBoot(registry, configuration);
injector.getInstance(SearchMain.class); var main = injector.getInstance(SearchMain.class);
injector.getInstance(Initialization.class).setReady(); 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

@ -5,20 +5,16 @@ import io.prometheus.client.Counter;
import io.prometheus.client.Histogram; import io.prometheus.client.Histogram;
import nu.marginalia.WebsiteUrl; import nu.marginalia.WebsiteUrl;
import nu.marginalia.search.svc.*; import nu.marginalia.search.svc.*;
import nu.marginalia.service.discovery.property.ServicePartition;
import nu.marginalia.service.server.BaseServiceParams; import nu.marginalia.service.server.BaseServiceParams;
import nu.marginalia.service.server.Service; import nu.marginalia.service.server.JoobyService;
import nu.marginalia.service.server.StaticResources; import nu.marginalia.service.server.StaticResources;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import spark.Route;
import spark.Spark;
import java.net.URLEncoder; import java.util.List;
import java.nio.charset.StandardCharsets;
public class SearchService extends Service { public class SearchService extends JoobyService {
private final WebsiteUrl websiteUrl; private final WebsiteUrl websiteUrl;
private final StaticResources staticResources; private final StaticResources staticResources;
@ -41,97 +37,103 @@ public class SearchService extends Service {
WebsiteUrl websiteUrl, WebsiteUrl websiteUrl,
StaticResources staticResources, StaticResources staticResources,
SearchFrontPageService frontPageService, SearchFrontPageService frontPageService,
SearchErrorPageService errorPageService,
SearchAddToCrawlQueueService addToCrawlQueueService, SearchAddToCrawlQueueService addToCrawlQueueService,
SearchSiteInfoService siteInfoService, SearchSiteInfoService siteInfoService,
SearchCrosstalkService crosstalkService, SearchCrosstalkService crosstalkService,
SearchBrowseService searchBrowseService, SearchBrowseService searchBrowseService,
SearchQueryService searchQueryService) SearchQueryService searchQueryService)
throws Exception throws Exception {
{ super(params,
super(params); ServicePartition.any(),
List.of(),
List.of(new SearchFrontPageService_(frontPageService),
new SearchQueryService_(searchQueryService),
new SearchSiteInfoService_(siteInfoService),
new SearchCrosstalkService_(crosstalkService),
new SearchAddToCrawlQueueService_(addToCrawlQueueService),
new SearchBrowseService_(searchBrowseService)
));
this.websiteUrl = websiteUrl; this.websiteUrl = websiteUrl;
this.staticResources = staticResources; this.staticResources = staticResources;
Spark.staticFiles.expireTime(600);
SearchServiceMetrics.get("/search", searchQueryService::pathSearch);
SearchServiceMetrics.get("/", frontPageService::render);
SearchServiceMetrics.get("/news.xml", frontPageService::renderNewsFeed);
SearchServiceMetrics.post("/site/suggest/", addToCrawlQueueService::suggestCrawling);
SearchServiceMetrics.get("/site-search/:site/*", this::siteSearchRedir);
SearchServiceMetrics.get("/site", siteInfoService::handleOverview);
SearchServiceMetrics.get("/site/:site", siteInfoService::handle);
SearchServiceMetrics.post("/site/:site", siteInfoService::handlePost);
SearchServiceMetrics.get("/explore", searchBrowseService::handleBrowseRandom);
SearchServiceMetrics.get("/explore/:site", searchBrowseService::handleBrowseSite);
SearchServiceMetrics.get("/crosstalk/", crosstalkService::handle);
SearchServiceMetrics.get("/:resource", this::serveStatic);
Spark.exception(Exception.class, (e,p,q) -> {
logger.error("Error during processing", e);
wmsa_search_service_error_count.labels(p.pathInfo(), p.requestMethod()).inc();
errorPageService.serveError(p, q);
});
// Add compression
Spark.after((rq, rs) -> {
rs.header("Content-Encoding", "gzip");
});
Spark.awaitInitialization();
} }
//
// SearchServiceMetrics.get("/search", searchQueryService::pathSearch);
// SearchServiceMetrics.get("/", frontPageService::render);
// SearchServiceMetrics.get("/news.xml", frontPageService::renderNewsFeed);
//
// SearchServiceMetrics.post("/site/suggest/", addToCrawlQueueService::suggestCrawling);
//
// SearchServiceMetrics.get("/site-search/:site/*", this::siteSearchRedir);
//
// SearchServiceMetrics.get("/site", siteInfoService::handleOverview);
// SearchServiceMetrics.get("/site/:site", siteInfoService::handle);
// SearchServiceMetrics.post("/site/:site", siteInfoService::handlePost);
//
// SearchServiceMetrics.get("/explore", searchBrowseService::handleBrowseRandom);
// SearchServiceMetrics.get("/explore/:site", searchBrowseService::handleBrowseSite);
//
// SearchServiceMetrics.get("/crosstalk/", crosstalkService::handle);
//
// SearchServiceMetrics.get("/:resource", this::serveStatic);
// Spark.exception(Exception.class, (e,p,q) -> {
// logger.error("Error during processing", e);
// wmsa_search_service_error_count.labels(p.pathInfo(), p.requestMethod()).inc();
// errorPageService.serveError(p, q);
// });
//
// // Add compression
// Spark.after((rq, rs) -> {
// rs.header("Content-Encoding", "gzip");
// });
//
// Spark.awaitInitialization();
//
//
/** Wraps a route with a timer and a counter */ // /** Wraps a route with a timer and a counter */
private static class SearchServiceMetrics implements Route { // private static class SearchServiceMetrics implements Route {
private final Route delegatedRoute; // private final Route delegatedRoute;
//
static void get(String path, Route route) { // static void get(String path, Route route) {
Spark.get(path, new SearchServiceMetrics(route)); // Spark.get(path, new SearchServiceMetrics(route));
} // }
static void post(String path, Route route) { // static void post(String path, Route route) {
Spark.post(path, new SearchServiceMetrics(route)); // Spark.post(path, new SearchServiceMetrics(route));
} // }
//
private SearchServiceMetrics(Route delegatedRoute) { // private SearchServiceMetrics(Route delegatedRoute) {
this.delegatedRoute = delegatedRoute; // this.delegatedRoute = delegatedRoute;
} // }
//
@Override // @Override
public Object handle(Request request, Response response) throws Exception { // public Object handle(Request request, Response response) throws Exception {
return wmsa_search_service_request_time // return wmsa_search_service_request_time
.labels(request.matchedPath(), request.requestMethod()) // .labels(request.matchedPath(), request.requestMethod())
.time(() -> delegatedRoute.handle(request, response)); // .time(() -> delegatedRoute.handle(request, response));
} // }
} // }
//
private Object serveStatic(Request request, Response response) { // private Object serveStatic(Request request, Response response) {
String resource = request.params("resource"); // String resource = request.params("resource");
staticResources.serveStatic("search", resource, request, response); // staticResources.serveStatic("search", resource, request, response);
return ""; // return "";
} // }
//
private Object siteSearchRedir(Request request, Response response) { // private Object siteSearchRedir(Request request, Response response) {
final String site = request.params("site"); // final String site = request.params("site");
final String searchTerms; // final String searchTerms;
//
if (request.splat().length == 0) searchTerms = ""; // if (request.splat().length == 0) searchTerms = "";
else searchTerms = request.splat()[0]; // else searchTerms = request.splat()[0];
//
final String query = URLEncoder.encode(String.format("%s site:%s", searchTerms, site), StandardCharsets.UTF_8).trim(); // final String query = URLEncoder.encode(String.format("%s site:%s", searchTerms, site), StandardCharsets.UTF_8).trim();
final String profile = request.queryParamOrDefault("profile", "yolo"); // final String profile = request.queryParamOrDefault("profile", "yolo");
//
response.redirect(websiteUrl.withPath("search?query="+query+"&profile="+profile)); // response.redirect(websiteUrl.withPath("search?query="+query+"&profile="+profile));
//
return ""; // return "";
} // }
} }

View File

@ -1,8 +1,8 @@
package nu.marginalia.search.command; package nu.marginalia.search.command;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.jooby.ModelAndView;
import nu.marginalia.search.command.commands.*; import nu.marginalia.search.command.commands.*;
import spark.Response;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -30,14 +30,14 @@ public class CommandEvaluator {
defaultCommand = search; defaultCommand = search;
} }
public Object eval(Response response, SearchParameters parameters) { public ModelAndView<?> eval(SearchParameters parameters) throws Exception {
for (var cmd : specialCommands) { for (var cmd : specialCommands) {
var maybe = cmd.process(response, parameters); var maybe = cmd.process(parameters);
if (maybe.isPresent()) if (maybe.isPresent())
return maybe.get(); return maybe.get();
} }
return defaultCommand.process(response, parameters).orElse(""); return defaultCommand.process(parameters).orElseThrow();
} }
} }

View File

@ -1,10 +1,9 @@
package nu.marginalia.search.command; package nu.marginalia.search.command;
import io.jooby.ModelAndView;
import spark.Response;
import java.util.Optional; import java.util.Optional;
public interface SearchCommandInterface { public interface SearchCommandInterface {
Optional<Object> process(Response response, SearchParameters parameters); Optional<ModelAndView<?>> process(SearchParameters parameters) throws Exception;
} }

View File

@ -6,11 +6,9 @@ import nu.marginalia.index.query.limit.QueryStrategy;
import nu.marginalia.index.query.limit.SpecificationLimit; import nu.marginalia.index.query.limit.SpecificationLimit;
import nu.marginalia.model.EdgeDomain; import nu.marginalia.model.EdgeDomain;
import nu.marginalia.search.model.SearchProfile; import nu.marginalia.search.model.SearchProfile;
import spark.Request;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.StringJoiner; import java.util.StringJoiner;
import static nu.marginalia.search.command.SearchRecentParameter.RECENT; import static nu.marginalia.search.command.SearchRecentParameter.RECENT;
@ -38,19 +36,6 @@ public record SearchParameters(WebsiteUrl url,
false, false,
page); page);
} }
public static SearchParameters forRequest(String queryString, WebsiteUrl url, Request request) {
return new SearchParameters(
url,
queryString,
SearchProfile.getSearchProfile(request.queryParams("profile")),
SearchJsParameter.parse(request.queryParams("js")),
SearchRecentParameter.parse(request.queryParams("recent")),
SearchTitleParameter.parse(request.queryParams("searchTitle")),
SearchAdtechParameter.parse(request.queryParams("adtech")),
"true".equals(request.queryParams("newfilter")),
Integer.parseInt(Objects.requireNonNullElse(request.queryParams("page"), "1"))
);
}
public String profileStr() { public String profileStr() {
return profile.filterId; return profile.filterId;

View File

@ -1,10 +1,10 @@
package nu.marginalia.search.command.commands; package nu.marginalia.search.command.commands;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import nu.marginalia.search.command.SearchCommandInterface; import nu.marginalia.search.command.SearchCommandInterface;
import nu.marginalia.search.command.SearchParameters; import nu.marginalia.search.command.SearchParameters;
import nu.marginalia.search.exceptions.RedirectException;
import spark.Response;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@ -24,7 +24,7 @@ public class BangCommand implements SearchCommandInterface {
} }
@Override @Override
public Optional<Object> process(Response response, SearchParameters parameters) { public Optional<ModelAndView<?>> process(SearchParameters parameters) {
for (var entry : bangsToPattern.entrySet()) { for (var entry : bangsToPattern.entrySet()) {
String bangPattern = entry.getKey(); String bangPattern = entry.getKey();
@ -34,7 +34,7 @@ public class BangCommand implements SearchCommandInterface {
if (match.isPresent()) { if (match.isPresent()) {
var url = String.format(redirectPattern, URLEncoder.encode(match.get(), StandardCharsets.UTF_8)); var url = String.format(redirectPattern, URLEncoder.encode(match.get(), StandardCharsets.UTF_8));
throw new RedirectException(url); new MapModelAndView("redirect.jte", Map.of("url", url));
} }
} }

View File

@ -1,10 +1,12 @@
package nu.marginalia.search.command.commands; package nu.marginalia.search.command.commands;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import nu.marginalia.search.command.SearchCommandInterface; import nu.marginalia.search.command.SearchCommandInterface;
import nu.marginalia.search.command.SearchParameters; import nu.marginalia.search.command.SearchParameters;
import spark.Response;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -19,7 +21,7 @@ public class BrowseRedirectCommand implements SearchCommandInterface {
} }
@Override @Override
public Optional<Object> process(Response response, SearchParameters parameters) { public Optional<ModelAndView<?>> process(SearchParameters parameters) {
if (!queryPatternPredicate.test(parameters.query())) { if (!queryPatternPredicate.test(parameters.query())) {
return Optional.empty(); return Optional.empty();
} }
@ -35,13 +37,9 @@ public class BrowseRedirectCommand implements SearchCommandInterface {
redirectPath = "/explore/" + word; redirectPath = "/explore/" + word;
} }
return Optional.of(""" return Optional.of(
<!DOCTYPE html> new MapModelAndView("/redirect.jte", Map.of("url", redirectPath))
<html lang="en"> );
<meta charset="UTF-8">
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0; %s">
""".formatted(redirectPath));
} }

View File

@ -1,12 +1,13 @@
package nu.marginalia.search.command.commands; package nu.marginalia.search.command.commands;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import nu.marginalia.search.JteRenderer; import nu.marginalia.search.JteRenderer;
import nu.marginalia.search.command.SearchCommandInterface; import nu.marginalia.search.command.SearchCommandInterface;
import nu.marginalia.search.command.SearchParameters; import nu.marginalia.search.command.SearchParameters;
import nu.marginalia.search.model.NavbarModel; import nu.marginalia.search.model.NavbarModel;
import nu.marginalia.search.svc.SearchUnitConversionService; import nu.marginalia.search.svc.SearchUnitConversionService;
import spark.Response;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -24,9 +25,10 @@ public class ConvertCommand implements SearchCommandInterface {
} }
@Override @Override
public Optional<Object> process(Response response, SearchParameters parameters) { public Optional<ModelAndView<?>> process(SearchParameters parameters) {
var conversion = searchUnitConversionService.tryConversion(parameters.query()); var conversion = searchUnitConversionService.tryConversion(parameters.query());
return conversion.map(s -> renderer.render("serp/unit-conversion.jte", Map.of( return conversion.map(s -> new MapModelAndView("serp/unit-conversion.jte",
Map.of(
"parameters", parameters, "parameters", parameters,
"navbar", NavbarModel.SEARCH, "navbar", NavbarModel.SEARCH,
"result", s) "result", s)

View File

@ -2,6 +2,8 @@
package nu.marginalia.search.command.commands; package nu.marginalia.search.command.commands;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import nu.marginalia.api.math.MathClient; import nu.marginalia.api.math.MathClient;
import nu.marginalia.api.math.model.DictionaryResponse; import nu.marginalia.api.math.model.DictionaryResponse;
import nu.marginalia.search.JteRenderer; import nu.marginalia.search.JteRenderer;
@ -10,7 +12,6 @@ import nu.marginalia.search.command.SearchParameters;
import nu.marginalia.search.model.NavbarModel; import nu.marginalia.search.model.NavbarModel;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import spark.Response;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -35,14 +36,14 @@ public class DefinitionCommand implements SearchCommandInterface {
} }
@Override @Override
public Optional<Object> process(Response response, SearchParameters parameters) { public Optional<ModelAndView<?>> process(SearchParameters parameters) {
if (!queryPatternPredicate.test(parameters.query())) { if (!queryPatternPredicate.test(parameters.query())) {
return Optional.empty(); return Optional.empty();
} }
DictionaryResponse result = lookupDefinition(parameters.query()); DictionaryResponse result = lookupDefinition(parameters.query());
return Optional.of(renderer.render("serp/dict-lookup.jte", return Optional.of(new MapModelAndView("serp/dict-lookup.jte",
Map.of("parameters", parameters, Map.of("parameters", parameters,
"result", result, "result", result,
"navbar", NavbarModel.SEARCH) "navbar", NavbarModel.SEARCH)

View File

@ -1,13 +1,14 @@
package nu.marginalia.search.command.commands; package nu.marginalia.search.command.commands;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import nu.marginalia.search.JteRenderer; import nu.marginalia.search.JteRenderer;
import nu.marginalia.search.SearchOperator; import nu.marginalia.search.SearchOperator;
import nu.marginalia.search.command.SearchCommandInterface; import nu.marginalia.search.command.SearchCommandInterface;
import nu.marginalia.search.command.SearchParameters; import nu.marginalia.search.command.SearchParameters;
import nu.marginalia.search.model.DecoratedSearchResults; import nu.marginalia.search.model.DecoratedSearchResults;
import nu.marginalia.search.model.NavbarModel; import nu.marginalia.search.model.NavbarModel;
import spark.Response;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
@ -26,16 +27,10 @@ public class SearchCommand implements SearchCommandInterface {
} }
@Override @Override
public Optional<Object> process(Response response, SearchParameters parameters) { public Optional<ModelAndView<?>> process(SearchParameters parameters) throws InterruptedException {
try { DecoratedSearchResults results = searchOperator.doSearch(parameters);
DecoratedSearchResults results = searchOperator.doSearch(parameters); return Optional.of(new MapModelAndView("serp/main.jte",
return Optional.of(jteRenderer.render("serp/main.jte", Map.of("results", results, "navbar", NavbarModel.SEARCH)
Map.of("results", results, "navbar", NavbarModel.SEARCH) ));
));
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return Optional.empty();
}
} }
} }

View File

@ -1,12 +1,14 @@
package nu.marginalia.search.command.commands; package nu.marginalia.search.command.commands;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import nu.marginalia.search.command.SearchCommandInterface; import nu.marginalia.search.command.SearchCommandInterface;
import nu.marginalia.search.command.SearchParameters; import nu.marginalia.search.command.SearchParameters;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import spark.Response;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -22,7 +24,7 @@ public class SiteRedirectCommand implements SearchCommandInterface {
} }
@Override @Override
public Optional<Object> process(Response response, SearchParameters parameters) { public Optional<ModelAndView<?>> process(SearchParameters parameters) {
if (!queryPatternPredicate.test(parameters.query())) { if (!queryPatternPredicate.test(parameters.query())) {
return Optional.empty(); return Optional.empty();
} }
@ -37,14 +39,8 @@ public class SiteRedirectCommand implements SearchCommandInterface {
default -> "info"; default -> "info";
}; };
return Optional.of(""" String url = "/site/%s?view=%s".formatted(domain, view);
<!DOCTYPE html> return Optional.of(new MapModelAndView("/redirect.jte", Map.of("url", url)));
<html lang="en">
<meta charset="UTF-8">
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0; url=/site/%s?view=%s">
""".formatted(domain, view)
);
} }
} }

View File

@ -1,14 +0,0 @@
package nu.marginalia.search.exceptions;
public class RedirectException extends RuntimeException {
public final String newUrl;
public RedirectException(String newUrl) {
this.newUrl = newUrl;
}
@Override
public StackTraceElement[] getStackTrace() {
return new StackTraceElement[0];
}
}

View File

@ -2,40 +2,41 @@ package nu.marginalia.search.svc;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import nu.marginalia.WebsiteUrl; import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import io.jooby.annotation.FormParam;
import io.jooby.annotation.POST;
import io.jooby.annotation.Path;
import nu.marginalia.db.DbDomainQueries; import nu.marginalia.db.DbDomainQueries;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import spark.Spark;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Map;
public class SearchAddToCrawlQueueService { public class SearchAddToCrawlQueueService {
private final DbDomainQueries domainQueries; private final DbDomainQueries domainQueries;
private final WebsiteUrl websiteUrl;
private final HikariDataSource dataSource; private final HikariDataSource dataSource;
private final Logger logger = LoggerFactory.getLogger(SearchAddToCrawlQueueService.class); private final Logger logger = LoggerFactory.getLogger(SearchAddToCrawlQueueService.class);
@Inject @Inject
public SearchAddToCrawlQueueService(DbDomainQueries domainQueries, public SearchAddToCrawlQueueService(DbDomainQueries domainQueries,
WebsiteUrl websiteUrl,
HikariDataSource dataSource) { HikariDataSource dataSource) {
this.domainQueries = domainQueries; this.domainQueries = domainQueries;
this.websiteUrl = websiteUrl;
this.dataSource = dataSource; this.dataSource = dataSource;
} }
public Object suggestCrawling(Request request, Response response) throws SQLException { @POST
logger.info("{}", request.queryParams()); @Path("/site/suggest/")
int id = Integer.parseInt(request.queryParams("id")); public ModelAndView<?> suggestCrawling(
boolean nomisclick = "on".equals(request.queryParams("nomisclick")); @FormParam int id,
@FormParam String nomisclick
) throws SQLException {
String domainName = getDomainName(id); String domainName = getDomainName(id);
if (nomisclick) { if ("on".equals(nomisclick)) {
logger.info("Adding {} to crawl queue", domainName); logger.info("Adding {} to crawl queue", domainName);
addToCrawlQueue(id); addToCrawlQueue(id);
} }
@ -43,9 +44,7 @@ public class SearchAddToCrawlQueueService {
logger.info("Nomisclick not set, not adding {} to crawl queue", domainName); logger.info("Nomisclick not set, not adding {} to crawl queue", domainName);
} }
response.redirect(websiteUrl.withPath("/site/" + domainName)); return new MapModelAndView("/redirect.jte", Map.of("url", "/site/"+domainName));
return "";
} }
private void addToCrawlQueue(int id) throws SQLException { private void addToCrawlQueue(int id) throws SQLException {
@ -62,7 +61,7 @@ public class SearchAddToCrawlQueueService {
private String getDomainName(int id) { private String getDomainName(int id) {
var domain = domainQueries.getDomain(id); var domain = domainQueries.getDomain(id);
if (domain.isEmpty()) if (domain.isEmpty())
Spark.halt(404); throw new IllegalArgumentException();
return domain.get().toString(); return domain.get().toString();
} }
} }

View File

@ -1,6 +1,11 @@
package nu.marginalia.search.svc; package nu.marginalia.search.svc;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import io.jooby.annotation.GET;
import io.jooby.annotation.Path;
import io.jooby.annotation.PathParam;
import nu.marginalia.api.domains.DomainInfoClient; import nu.marginalia.api.domains.DomainInfoClient;
import nu.marginalia.api.domains.model.SimilarDomain; import nu.marginalia.api.domains.model.SimilarDomain;
import nu.marginalia.browse.DbBrowseDomainsRandom; import nu.marginalia.browse.DbBrowseDomainsRandom;
@ -12,10 +17,7 @@ import nu.marginalia.model.EdgeDomain;
import nu.marginalia.search.JteRenderer; import nu.marginalia.search.JteRenderer;
import nu.marginalia.search.model.NavbarModel; import nu.marginalia.search.model.NavbarModel;
import nu.marginalia.search.results.BrowseResultCleaner; import nu.marginalia.search.results.BrowseResultCleaner;
import spark.Request;
import spark.Response;
import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -47,16 +49,19 @@ public class SearchBrowseService {
this.browseResultCleaner = browseResultCleaner; this.browseResultCleaner = browseResultCleaner;
} }
public String handleBrowseRandom(Request request, Response response) throws IOException { @GET
return jteRenderer.render("explore/main.jte", @Path("/explore")
public ModelAndView<?> handleBrowseRandom() {
return new MapModelAndView("explore/main.jte",
Map.of("navbar", NavbarModel.EXPLORE, Map.of("navbar", NavbarModel.EXPLORE,
"results", getRandomEntries(1) "results", getRandomEntries(1)
) )
); );
} }
public String handleBrowseSite(Request request, Response response) throws Exception { @GET
String domainName = request.params("site"); @Path("/explore/{domainName}")
public ModelAndView<?> handleBrowseSite(@PathParam String domainName) throws Exception {
BrowseResultSet entries; BrowseResultSet entries;
try { try {
@ -66,7 +71,7 @@ public class SearchBrowseService {
entries = new BrowseResultSet(List.of(), domainName); entries = new BrowseResultSet(List.of(), domainName);
} }
return jteRenderer.render("explore/main.jte", return new MapModelAndView("explore/main.jte",
Map.of("navbar", NavbarModel.EXPLORE, Map.of("navbar", NavbarModel.EXPLORE,
"results", entries "results", entries
) )

View File

@ -1,6 +1,11 @@
package nu.marginalia.search.svc; package nu.marginalia.search.svc;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import io.jooby.annotation.GET;
import io.jooby.annotation.Path;
import io.jooby.annotation.QueryParam;
import nu.marginalia.search.JteRenderer; import nu.marginalia.search.JteRenderer;
import nu.marginalia.search.SearchOperator; import nu.marginalia.search.SearchOperator;
import nu.marginalia.search.model.NavbarModel; import nu.marginalia.search.model.NavbarModel;
@ -9,8 +14,6 @@ import nu.marginalia.search.model.UrlDetails;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException; import java.sql.SQLException;
@ -29,16 +32,15 @@ public class SearchCrosstalkService {
this.renderer = renderer; this.renderer = renderer;
} }
public Object handle(Request request, Response response) throws SQLException { @GET
String domains = request.queryParams("domains"); @Path("/crosstalk")
public ModelAndView<?> crosstalk(@QueryParam String domains) throws SQLException {
String[] parts = StringUtils.split(domains, ','); String[] parts = StringUtils.split(domains, ',');
if (parts.length != 2) { if (parts.length != 2) {
throw new IllegalArgumentException("Expected exactly two domains"); throw new IllegalArgumentException("Expected exactly two domains");
} }
response.type("text/html");
for (int i = 0; i < parts.length; i++) { for (int i = 0; i < parts.length; i++) {
parts[i] = parts[i].trim(); parts[i] = parts[i].trim();
} }
@ -48,7 +50,7 @@ public class SearchCrosstalkService {
CrosstalkResult model = new CrosstalkResult(parts[0], parts[1], resAtoB.results, resBtoA.results); CrosstalkResult model = new CrosstalkResult(parts[0], parts[1], resAtoB.results, resBtoA.results);
return renderer.render( return new MapModelAndView(
"siteinfo/crosstalk.jte", "siteinfo/crosstalk.jte",
Map.of("model", model, Map.of("model", model,
"navbar", NavbarModel.SITEINFO)); "navbar", NavbarModel.SITEINFO));

View File

@ -1,41 +1,28 @@
package nu.marginalia.search.svc; package nu.marginalia.search.svc;
import com.google.inject.Inject; import com.google.inject.Inject;
import nu.marginalia.WebsiteUrl; import io.jooby.MapModelAndView;
import nu.marginalia.search.JteRenderer; import io.jooby.ModelAndView;
import nu.marginalia.search.command.SearchParameters; import nu.marginalia.search.command.SearchParameters;
import nu.marginalia.search.model.NavbarModel; import nu.marginalia.search.model.NavbarModel;
import nu.marginalia.search.model.SearchErrorMessageModel; import nu.marginalia.search.model.SearchErrorMessageModel;
import nu.marginalia.search.model.SearchFilters; import nu.marginalia.search.model.SearchFilters;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
public class SearchErrorPageService { public class SearchErrorPageService {
private final WebsiteUrl websiteUrl;
private final JteRenderer jteRenderer;
private final Logger logger = LoggerFactory.getLogger(getClass()); private final Logger logger = LoggerFactory.getLogger(getClass());
@Inject @Inject
public SearchErrorPageService(WebsiteUrl websiteUrl, public SearchErrorPageService() throws IOException {
JteRenderer jteRenderer) throws IOException {
this.websiteUrl = websiteUrl;
this.jteRenderer = jteRenderer;
} }
public void serveError(Request request, Response rsp) { public ModelAndView<?> serveError(SearchParameters parameters) {
var params = SearchParameters.forRequest( return new MapModelAndView("serp/error.jte",
request.queryParamOrDefault("query", ""),
websiteUrl,
request);
rsp.body(jteRenderer.render("serp/error.jte",
Map.of("navbar", NavbarModel.LIMBO, Map.of("navbar", NavbarModel.LIMBO,
"model", new SearchErrorMessageModel( "model", new SearchErrorMessageModel(
"An error occurred when communicating with the search engine index.", "An error occurred when communicating with the search engine index.",
@ -44,11 +31,11 @@ public class SearchErrorPageService {
an upgrade. The index typically takes a about two or three minutes an upgrade. The index typically takes a about two or three minutes
to reload from a cold restart. Thanks for your patience. to reload from a cold restart. Thanks for your patience.
""", """,
params, parameters,
new SearchFilters(params) new SearchFilters(parameters)
) )
) )
)); );
} }
} }

View File

@ -3,30 +3,24 @@ package nu.marginalia.search.svc;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import io.jooby.MapModelAndView;
import io.jooby.annotation.GET;
import io.jooby.annotation.Path;
import nu.marginalia.WebsiteUrl; import nu.marginalia.WebsiteUrl;
import nu.marginalia.search.JteRenderer;
import nu.marginalia.search.model.NavbarModel; import nu.marginalia.search.model.NavbarModel;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import java.io.IOException;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
/** Renders the front page (index) */ /** Renders the front page (index) */
@Singleton @Singleton
public class SearchFrontPageService { public class SearchFrontPageService {
private final HikariDataSource dataSource; private final HikariDataSource dataSource;
private final JteRenderer jteRenderer;
private final SearchQueryCountService searchVisitorCount; private final SearchQueryCountService searchVisitorCount;
private final WebsiteUrl websiteUrl; private final WebsiteUrl websiteUrl;
@ -34,21 +28,20 @@ public class SearchFrontPageService {
@Inject @Inject
public SearchFrontPageService(HikariDataSource dataSource, public SearchFrontPageService(HikariDataSource dataSource,
JteRenderer jteRenderer, SearchQueryCountService searchVisitorCount,
SearchQueryCountService searchVisitorCount, WebsiteUrl websiteUrl WebsiteUrl websiteUrl
) throws IOException { ) {
this.dataSource = dataSource; this.dataSource = dataSource;
this.jteRenderer = jteRenderer;
this.searchVisitorCount = searchVisitorCount; this.searchVisitorCount = searchVisitorCount;
this.websiteUrl = websiteUrl; this.websiteUrl = websiteUrl;
} }
public String render(Request request, Response response) { @GET
response.header("Cache-control", "public,max-age=3600"); @Path("/")
public MapModelAndView render() {
return jteRenderer.render("serp/first.jte", return new MapModelAndView("serp/first.jte")
Map.of("navbar", NavbarModel.SEARCH, "websiteUrl", websiteUrl) .put("navbar", NavbarModel.SEARCH)
); .put("websiteUrl", websiteUrl);
} }
@ -77,6 +70,7 @@ public class SearchFrontPageService {
return items; return items;
} }
/* FIXME
public Object renderNewsFeed(Request request, Response response) { public Object renderNewsFeed(Request request, Response response) {
List<NewsItem> newsItems = getNewsItems(); List<NewsItem> newsItems = getNewsItems();
@ -112,7 +106,7 @@ public class SearchFrontPageService {
response.type("application/rss+xml"); response.type("application/rss+xml");
return sb.toString(); return sb.toString();
} }*/
private record IndexModel(List<NewsItem> news, int searchPerMinute) { } private record IndexModel(List<NewsItem> news, int searchPerMinute) { }
private record NewsItem(String title, String url, String source, LocalDate date) {} private record NewsItem(String title, String url, String source, LocalDate date) {}

View File

@ -1,14 +1,17 @@
package nu.marginalia.search.svc; package nu.marginalia.search.svc;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.jooby.ModelAndView;
import io.jooby.annotation.GET;
import io.jooby.annotation.Path;
import io.jooby.annotation.QueryParam;
import nu.marginalia.WebsiteUrl; import nu.marginalia.WebsiteUrl;
import nu.marginalia.search.command.CommandEvaluator; import nu.marginalia.search.command.*;
import nu.marginalia.search.command.SearchParameters; import nu.marginalia.search.model.SearchProfile;
import nu.marginalia.search.exceptions.RedirectException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response; import java.util.Objects;
public class SearchQueryService { public class SearchQueryService {
@ -27,36 +30,34 @@ public class SearchQueryService {
this.searchCommandEvaulator = searchCommandEvaulator; this.searchCommandEvaulator = searchCommandEvaulator;
} }
public Object pathSearch(Request request, Response response) { @GET
@Path("/search")
public ModelAndView<?> pathSearch(
@QueryParam String query,
@QueryParam String profile,
@QueryParam String js,
@QueryParam String recent,
@QueryParam String title,
@QueryParam String adtech,
@QueryParam Integer page
) {
try { try {
return searchCommandEvaulator.eval(response, parseParameters(request)); SearchParameters parameters = new SearchParameters(websiteUrl,
} query,
catch (RedirectException ex) { SearchProfile.getSearchProfile(profile),
response.redirect(ex.newUrl); SearchJsParameter.parse(js),
SearchRecentParameter.parse(recent),
SearchTitleParameter.parse(title),
SearchAdtechParameter.parse(adtech),
false,
Objects.requireNonNullElse(page,1));
return searchCommandEvaulator.eval(parameters);
} }
catch (Exception ex) { catch (Exception ex) {
logger.error("Error", ex); logger.error("Error", ex);
errorPageService.serveError(request, response); return errorPageService.serveError(SearchParameters.defaultsForQuery(websiteUrl, query, page));
}
return "";
}
private SearchParameters parseParameters(Request request) {
try {
final String queryParam = request.queryParams("query");
if (null == queryParam || queryParam.isBlank()) {
throw new RedirectException(websiteUrl.url());
}
return SearchParameters.forRequest(queryParam.trim(), websiteUrl, request);
}
catch (Exception ex) {
// Bots keep sending bad requests, suppress the error otherwise it will
// fill up the logs.
throw new RedirectException(websiteUrl.url());
} }
} }
} }

View File

@ -2,6 +2,9 @@ package nu.marginalia.search.svc;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import io.jooby.annotation.*;
import nu.marginalia.api.domains.DomainInfoClient; import nu.marginalia.api.domains.DomainInfoClient;
import nu.marginalia.api.domains.model.DomainInformation; import nu.marginalia.api.domains.model.DomainInformation;
import nu.marginalia.api.domains.model.SimilarDomain; import nu.marginalia.api.domains.model.SimilarDomain;
@ -21,8 +24,6 @@ import nu.marginalia.search.model.UrlDetails;
import nu.marginalia.search.svc.SearchFlagSiteService.FlagSiteFormData; import nu.marginalia.search.svc.SearchFlagSiteService.FlagSiteFormData;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.*; import java.util.*;
@ -68,17 +69,12 @@ public class SearchSiteInfoService {
this.jteRenderer = jteRenderer; this.jteRenderer = jteRenderer;
} }
public Object handleOverview(Request request, Response response) { @GET
String domainName = request.queryParams("domain"); @Path("/site")
if (domainName != null) { public ModelAndView<?> handleOverview(@PathParam String domain) {
if (domain != null) {
// redirect to /site/domainName // redirect to /site/domainName
return """ return new MapModelAndView("/redirect.jte", Map.of("url", "/site/"+domain));
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0; url=/site/%s">
""".formatted(domainName);
} }
List<SiteOverviewModel.DiscoveredDomain> domains = new ArrayList<>(); List<SiteOverviewModel.DiscoveredDomain> domains = new ArrayList<>();
@ -95,7 +91,7 @@ public class SearchSiteInfoService {
throw new RuntimeException(); throw new RuntimeException();
} }
return jteRenderer.render("siteinfo/start.jte", return new MapModelAndView("siteinfo/start.jte",
Map.of("navbar", NavbarModel.SITEINFO, Map.of("navbar", NavbarModel.SITEINFO,
"model", new SiteOverviewModel(domains))); "model", new SiteOverviewModel(domains)));
} }
@ -104,15 +100,20 @@ public class SearchSiteInfoService {
public record DiscoveredDomain(String name, String timestamp) {} public record DiscoveredDomain(String name, String timestamp) {}
} }
public Object handle(Request request, Response response) throws SQLException { @GET
String domainName = request.params("site"); @Path("/site/{domainName}")
String view = request.queryParamOrDefault("view", "info"); public ModelAndView<?> handle(
@PathParam String domainName,
@QueryParam String view,
@QueryParam Integer page
) throws SQLException {
if (null == domainName || domainName.isBlank()) { if (null == domainName || domainName.isBlank()) {
return null; return null;
} }
int page = Integer.parseInt(request.queryParamOrDefault("page", "1")); page = Objects.requireNonNullElse(page, 1);
view = Objects.requireNonNullElse(view, "info");
SiteInfoModel model = switch (view) { SiteInfoModel model = switch (view) {
case "links" -> listLinks(domainName, page); case "links" -> listLinks(domainName, page);
@ -122,13 +123,20 @@ public class SearchSiteInfoService {
default -> listInfo(domainName); default -> listInfo(domainName);
}; };
return jteRenderer.render("siteinfo/main.jte", return new MapModelAndView("siteinfo/main.jte",
Map.of("model", model, "navbar", NavbarModel.SITEINFO)); Map.of("model", model, "navbar", NavbarModel.SITEINFO));
} }
public Object handlePost(Request request, Response response) throws SQLException { @POST
String domainName = request.params("site"); @Path("/site/{domainName}")
String view = request.queryParamOrDefault("view", "info"); public ModelAndView<?> handleComplaint(
@PathParam String domainName,
@QueryParam String view,
@FormParam String category,
@FormParam String description,
@FormParam String samplequery
) throws SQLException {
if (null == domainName || domainName.isBlank()) { if (null == domainName || domainName.isBlank()) {
return null; return null;
@ -141,9 +149,9 @@ public class SearchSiteInfoService {
FlagSiteFormData formData = new FlagSiteFormData( FlagSiteFormData formData = new FlagSiteFormData(
domainId, domainId,
request.queryParams("category"), category,
request.queryParams("description"), description,
request.queryParams("sampleQuery") samplequery
); );
flagSiteService.insertComplaint(formData); flagSiteService.insertComplaint(formData);
@ -151,7 +159,7 @@ public class SearchSiteInfoService {
var model = new ReportDomain(domainName, domainId, complaints, List.of(), true); var model = new ReportDomain(domainName, domainId, complaints, List.of(), true);
return jteRenderer.render("siteinfo/main.jte", return new MapModelAndView("siteinfo/main.jte",
Map.of("model", model, "navbar", NavbarModel.SITEINFO)); Map.of("model", model, "navbar", NavbarModel.SITEINFO));
} }

View File

@ -0,0 +1,14 @@
@param String url
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0; ${url}">
<h1>Redirecting...</h1>
If this does not work, please try this link
<p></p>
<a href="${url}">${url}</a>.
</html>

View File

@ -5,6 +5,7 @@
@param NavbarModel navbar @param NavbarModel navbar
@param WebsiteUrl websiteUrl @param WebsiteUrl websiteUrl
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">

View File

@ -1,3 +1,4 @@
@import nu.marginalia.search.svc.SearchFlagSiteService
@import nu.marginalia.search.svc.SearchSiteInfoService.* @import nu.marginalia.search.svc.SearchSiteInfoService.*
@param ReportDomain reportDomain @param ReportDomain reportDomain
@ -14,7 +15,6 @@
instead of using this form. instead of using this form.
</div> </div>
@else @else
<form class="space-y-6 p-4" method="post"> <form class="space-y-6 p-4" method="post">
<div> <div>
<label class="block text-sm font-medium text-slate-700 mb-1"> <label class="block text-sm font-medium text-slate-700 mb-1">
@ -22,10 +22,9 @@
</label> </label>
<select required name="category" class="w-full px-3 py-2 bg-white border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"> <select required name="category" class="w-full px-3 py-2 bg-white border border-slate-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
<option value="">Select issue type...</option> <option value="">Select issue type...</option>
<option value="spam">Spam</option> @for (SearchFlagSiteService.CategoryItem item : SearchFlagSiteService.categories)
<option value="dead-link">Dead Link</option> <option value="${item.categoryName()}">${item.categoryDesc()}</option>
<option value="inappropriate">Inappropriate Content</option> @endfor
<option value="other">Other</option>
</select> </select>
</div> </div>

View File

@ -570,6 +570,10 @@ video {
visibility: visible; visibility: visible;
} }
.static {
position: static;
}
.fixed { .fixed {
position: fixed; position: fixed;
} }
@ -586,12 +590,8 @@ video {
inset: 0px; inset: 0px;
} }
.bottom-0 { .bottom-10 {
bottom: 0px; bottom: 2.5rem;
}
.left-0 {
left: 0px;
} }
.right-5 { .right-5 {
@ -602,12 +602,12 @@ video {
top: 0px; top: 0px;
} }
.top-4 { .top-2 {
top: 1rem; top: 0.5rem;
} }
.top-5 { .top-4 {
top: 1.25rem; top: 1rem;
} }
.z-50 { .z-50 {
@ -725,6 +725,10 @@ video {
margin-top: 0.25rem; margin-top: 0.25rem;
} }
.mt-10 {
margin-top: 2.5rem;
}
.mt-2 { .mt-2 {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
@ -789,6 +793,10 @@ video {
width: 16rem; width: 16rem;
} }
.w-96 {
width: 24rem;
}
.w-full { .w-full {
width: 100%; width: 100%;
} }
@ -829,6 +837,10 @@ video {
flex-grow: 1; flex-grow: 1;
} }
.basis-1\/2 {
flex-basis: 50%;
}
.cursor-pointer { .cursor-pointer {
cursor: pointer; cursor: pointer;
} }
@ -901,6 +913,12 @@ video {
gap: 1.5rem; gap: 1.5rem;
} }
.space-x-0 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0px * var(--tw-space-x-reverse));
margin-left: calc(0px * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-1 > :not([hidden]) ~ :not([hidden]) { .space-x-1 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0; --tw-space-x-reverse: 0;
margin-right: calc(0.25rem * var(--tw-space-x-reverse)); margin-right: calc(0.25rem * var(--tw-space-x-reverse));
@ -1001,6 +1019,10 @@ video {
border-radius: 9999px; border-radius: 9999px;
} }
.rounded-lg {
border-radius: 0.5rem;
}
.rounded-md { .rounded-md {
border-radius: 0.375rem; border-radius: 0.375rem;
} }
@ -1009,6 +1031,10 @@ video {
border-radius: 0.125rem; border-radius: 0.125rem;
} }
.rounded-xl {
border-radius: 0.75rem;
}
.border { .border {
border-width: 1px; border-width: 1px;
} }
@ -1017,6 +1043,10 @@ video {
border-bottom-width: 1px; border-bottom-width: 1px;
} }
.border-none {
border-style: none;
}
.border-blue-200 { .border-blue-200 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(191 219 254 / var(--tw-border-opacity, 1)); border-color: rgb(191 219 254 / var(--tw-border-opacity, 1));
@ -1091,10 +1121,6 @@ video {
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1)); background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
} }
.bg-gray-50\/90 {
background-color: rgb(249 250 251 / 0.9);
}
.bg-green-50 { .bg-green-50 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1)); background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1));
@ -1478,6 +1504,12 @@ video {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
} }
.shadow-lg {
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-md { .shadow-md {
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
@ -1490,6 +1522,14 @@ video {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
} }
.outline-1 {
outline-width: 1px;
}
.outline-margeblue {
outline-color: #3e5f6f;
}
.filter { .filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
} }
@ -1564,6 +1604,15 @@ video {
--tw-ring-offset-width: 2px; --tw-ring-offset-width: 2px;
} }
.active\:text-slate-200:active {
--tw-text-opacity: 1;
color: rgb(226 232 240 / var(--tw-text-opacity, 1));
}
.active\:outline:active {
outline-style: solid;
}
.has-\[\:checked\]\:bg-gray-100:has(:checked) { .has-\[\:checked\]\:bg-gray-100:has(:checked) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1)); background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
@ -1590,10 +1639,6 @@ video {
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:static {
position: static;
}
.sm\:m-0 { .sm\:m-0 {
margin: 0px; margin: 0px;
} }
@ -1634,21 +1679,14 @@ video {
margin-bottom: calc(0px * var(--tw-space-y-reverse)); margin-bottom: calc(0px * var(--tw-space-y-reverse));
} }
.sm\:border-none { .sm\:space-y-6 > :not([hidden]) ~ :not([hidden]) {
border-style: none; --tw-space-y-reverse: 0;
margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
} }
.sm\:bg-white { .sm\:p-4 {
--tw-bg-opacity: 1; padding: 1rem;
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
}
.sm\:p-0 {
padding: 0px;
}
.sm\:p-3 {
padding: 0.75rem;
} }
.sm\:px-2 { .sm\:px-2 {
@ -1673,6 +1711,14 @@ video {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.md\:ml-16 {
margin-left: 4rem;
}
.md\:mr-16 {
margin-right: 4rem;
}
.md\:mr-8 { .md\:mr-8 {
margin-right: 2rem; margin-right: 2rem;
} }
@ -1685,6 +1731,14 @@ video {
display: inline; display: inline;
} }
.md\:hidden {
display: none;
}
.md\:w-32 {
width: 8rem;
}
.md\:w-64 { .md\:w-64 {
width: 16rem; width: 16rem;
} }
@ -1709,6 +1763,10 @@ video {
place-items: start; place-items: start;
} }
.md\:place-items-baseline {
place-items: baseline;
}
.md\:gap-8 { .md\:gap-8 {
gap: 2rem; gap: 2rem;
} }
@ -1725,6 +1783,10 @@ video {
margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse))); margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
} }
.md\:p-4 {
padding: 1rem;
}
.md\:px-4 { .md\:px-4 {
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;

View File

@ -1,17 +0,0 @@
<?xml version="1.0"?>
<!-- CC0 -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 455.731 455.731" xml:space="preserve">
<g>
<rect x="0" y="0" style="fill:#F78422;" width="455.731" height="455.731"/>
<g>
<path style="fill:#FFFFFF;" d="M296.208,159.16C234.445,97.397,152.266,63.382,64.81,63.382v64.348
c70.268,0,136.288,27.321,185.898,76.931c49.609,49.61,76.931,115.63,76.931,185.898h64.348
C391.986,303.103,357.971,220.923,296.208,159.16z"/>
<path style="fill:#FFFFFF;" d="M64.143,172.273v64.348c84.881,0,153.938,69.056,153.938,153.939h64.348
C282.429,270.196,184.507,172.273,64.143,172.273z"/>
<circle style="fill:#FFFFFF;" cx="109.833" cy="346.26" r="46.088"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 891 B

View File

@ -1,831 +0,0 @@
:root {
color-scheme: light;
--clr-bg-page: hsl(60, 42%, 95%); // $nicotine-light
--clr-bg-ui: hsl(0, 0%, 100%);
--clr-text-ui: #000; // $fg-dark
--clr-bg-theme: hsl(200, 28%, 34%); // $highlight-light
--clr-text-theme: #fff; // $fg-light
--clr-bg-highlight: hsl(0, 0%, 93%); // $highlight-light2
--clr-text-highlight: #111111;
--clr-bg-accent: hsl(63, 19%, 61%); // $nicotine-dark
--clr-border-accent: hsl(63, 19%, 35%);
--clr-border: #aaa; // $border-color2
--clr-shadow: var(--clr-border);
--clr-link: #0066cc;
--clr-link-visited: #531a89;
--clr-heading-link-visited: #fcc; // $visited
--font-family: sans-serif;
--font-size: 14px;
--font-family-heading: serif; // $heading-fonts
}
@mixin dark-theme-mixin {
color-scheme: dark;
--clr-bg-page: hsl(0, 0%, 6%);
--clr-bg-ui: hsl(0, 0%, 18%);
--clr-text-ui: #ddd;
--clr-bg-theme: hsl(0, 0%, 2%);
--clr-text-theme: var(--clr-text-ui);
--clr-bg-highlight: hsl(0, 0%, 11%);
--clr-text-highlight: #fff;
--clr-bg-accent: hsl(200, 32%, 28%);
--clr-border-accent: hsl(200, 8%, 12%);
--clr-border: hsl(0, 0%, 30%);
--clr-shadow: #000;
--clr-link: #8a8aff;
--clr-link-visited: #ffadff;
--clr-heading-link-visited: var(--clr-link-visited);
}
:root[data-theme='dark'] {
@include dark-theme-mixin;
}
// Makes theme match the user's OS preference when JS is disabled
@media (prefers-color-scheme: dark) {
:root:not([data-has-js="true"]) {
@include dark-theme-mixin;
}
}
* {
box-sizing: border-box;
}
a {
color: var(--clr-link);
}
a:visited {
color: var(--clr-link-visited);
}
input, textarea, select {
color: inherit;
}
h1 a, h2 a {
color: var(--clr-text-theme);
}
h1 a:visited, h2 a:visited {
color: var(--clr-heading-link-visited);
}
progress {
width: 10ch;
}
body {
background-color: var(--clr-bg-page);
color: var(--clr-text-ui);
font-family: var(--font-family);
font-size: var(--font-size);
line-height: 1.6;
margin-left: auto;
margin-right: auto;
max-width: 120ch;
padding: 0;
}
#frontpage {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto 1fr;
grid-gap: 1ch;
align-items: start;
justify-content: start;
margin-top: 1ch;
margin-bottom: 1ch;
// named grid areas
grid-template-areas:
"frontpage-about frontpage-news"
"frontpage-tips frontpage-news";
@media (max-device-width: 624px) {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
grid-gap: 1ch;
align-items: start;
justify-content: start;
margin-top: 1ch;
margin-bottom: 1ch;
// named grid areas
grid-template-areas:
"frontpage-about"
"frontpage-tips"
"frontpage-news";
* { max-width: unset !important; min-width: unset !important; }
}
#frontpage-news {
grid-area: frontpage-news;
max-width: 40ch;
@extend .dialog;
}
#frontpage-about {
grid-area: frontpage-about;
min-width: 40ch;
@extend .dialog;
}
#frontpage-tips {
grid-area: frontpage-tips;
min-width: 40ch;
@extend .dialog;
}
}
#siteinfo-nav {
display: block;
width: 100%;
@extend .dialog;
padding: 0.25ch !important;
margin-top: 1.5ch;
ul {
list-style: none;
padding: 0;
margin: 1ch;
li {
display: inline;
padding: 1ch;
background-color: var(--clr-bg-highlight);
a {
text-decoration: none;
display: inline-block;
color: var(--clr-text-highlight);
}
}
li.current {
background-color: var(--clr-bg-theme);
a {
color: var(--clr-text-theme);
}
}
}
}
.dialog {
border: 1px solid var(--clr-border);
box-shadow: 0 0 1ch var(--clr-shadow);
background-color: var(--clr-bg-ui);
padding: 1ch;
h2 {
margin: 0;
font-family: sans-serif;
font-weight: normal;
padding: 0.5ch;
font-size: 12pt;
background-color: var(--clr-bg-theme);
color: var(--clr-text-theme);
}
}
header {
background-color: var(--clr-bg-accent);
border: 1px solid var(--clr-border-accent);
color: var(--clr-text-ui);
box-shadow: 0 0 0.5ch var(--clr-shadow);
margin-bottom: 1ch;
display: flex;
align-items: center;
justify-content: space-between;
nav {
a {
text-decoration: none;
color: var(--clr-text-ui);
padding: .5ch;
display: inline-block;
}
a:visited {
color: var(--clr-text-ui);
}
a.extra {
background: #ccc linear-gradient(45deg,
hsl(0, 100%, 70%) 0%,
hsl(120, 100%, 70%) 50%,
hsl(240, 100%, 70%) 100%);
color: black;
text-shadow: 0 0 0.5ch #fff;
}
a:hover, a:focus {
background: var(--clr-bg-theme);
color: var(--clr-text-theme);
}
}
}
#theme {
padding: .5ch;
display: none;
[data-has-js='true'] & {
display: block;
}
}
#complaint {
@extend .dialog;
max-width: 60ch;
margin-left: auto;
margin-right: auto;
margin-top: 2ch;
textarea {
width: 100%;
height: 10ch;
}
}
#siteinfo {
margin-top: 1ch;
display: flex;
gap: 1ch;
flex-grow: 0.5;
flex-shrink: 0.5;
flex-basis: 10ch 10ch;
flex-direction: row;
flex-wrap: wrap;
align-content: stretch;
align-items: stretch;
justify-content: stretch;
#index-info, #link-info {
width: 32ch;
@extend .dialog;
}
#screenshot {
@extend .dialog;
}
#screenshot img {
width: 30ch;
height: 22.5ch;
}
}
.infobox {
h2 {
@extend .heading;
}
background-color: var(--clr-bg-ui);
padding: 1ch;
margin: 1ch;
border: 1px solid var(--clr-border);
box-shadow: 0 0 1ch var(--clr-shadow);
}
section.cards {
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding-top: 1ch;
gap: 2ch;
justify-content: flex-start;
.card {
background-color: var(--clr-bg-ui);
border-left: 1px solid #ecb;
border-top: 1px solid #ecb;
box-shadow: var(--clr-shadow) 0 0 5px;
h2 {
@extend .heading;
word-break: break-word;
}
h2 a {
display: block !important;
color: inherit;
text-decoration: none;
}
a:focus img {
filter: sepia(100%);
box-shadow: #444 0px 0px 20px;
}
a:focus:not(.nofocus) {
background-color: black;
color: white;
}
.description {
padding-left: 1ch;
padding-right: 1ch;
overflow: auto;
-webkit-hyphens: auto;
-moz-hyphens: auto;
-ms-hyphens: auto;
hyphens: auto;
}
img {
width: 28ch;
height: auto;
}
.info {
padding-left: 1ch;
padding-right: 1ch;
line-height: 1.6;
}
[data-theme='dark'] & {
border: 1px solid var(--clr-border);
}
}
}
.positions {
box-shadow: 0 0 2px var(--clr-shadow);
backdrop-filter: brightness(90%);
color: var(--clr-text-highlight);
padding: 2px;
margin-right: -1ch;
margin-left: 1ch;
}
footer {
clear: both;
padding: 2ch;
margin: 16ch 0 0 0;
font-size: 12pt;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
h1 {
font-weight: normal;
border-bottom: 4px solid var(--clr-bg-theme);
}
h2 {
font-size: 14pt;
font-weight: normal;
border-bottom: 2px solid var(--clr-bg-theme);
width: 80%;
}
section {
line-height: 1.5;
flex-basis: 40ch;
flex-grow: 1.1;
background-color: var(--clr-bg-ui);
border-left: 1px solid var(--clr-border);
box-shadow: -1px -1px 5px var(--clr-shadow);
padding-left: 1ch;
padding-right: 1ch;
margin-left: 1ch;
padding-bottom: 1ch;
margin-bottom: 1ch;
}
}
#mcfeast, #menu-close {
display: none;
}
.shadowbox {
box-shadow: 0 0 1ch var(--clr-shadow);
border: 1px solid var(--clr-border);
}
.heading {
margin: 0;
padding: 0.5ch;
background-color: var(--clr-bg-theme);
border-bottom: 1px solid var(--clr-border);
font-family: var(--font-family-heading);
font-weight: normal;
color: var(--clr-text-theme);
font-size: 12pt;
word-break: break-word;
}
.sidebar-narrow {
display: grid;
grid-template-columns: auto max-content;
grid-gap: 1ch;
align-items: start;
}
#crosstalk-view {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto 1fr;
grid-gap: 1ch;
align-content: start;
justify-content: start;
align-items: start;
}
#similar-view {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto 1fr;
grid-gap: 1ch;
align-content: start;
justify-content: start;
align-items: start;
table {
th {
text-align: left;
}
}
.screenshot {
width: 100%;
height: auto;
}
}
#similar-view[data-layout="lopsided"] {
#similar-info {
@extend .dialog;
grid-column: 1;
grid-row: 1 / span 2;
}
#similar-domains {
@extend .dialog;
grid-column: 2;
grid-row: 1;
}
#similar-links {
@extend .dialog;
grid-row: 2;
grid-column: 2;
}
}
#similar-view[data-layout="balanced"] {
#similar-info {
@extend .dialog;
}
#similar-domains {
grid-row: span 2;
@extend .dialog;
}
#similar-links {
@extend .dialog;
}
}
@media (max-device-width: 900px) {
#similar-view, #crosstalk-view {
display: block;
* {
margin-bottom: 1ch;
}
}
}
@media (max-device-width: 840px) {
section.cards {
display: block;
.card {
margin-bottom: 2ch;
img {
width: 100% !important;
height: auto;
}
}
}
}
#search-box {
@extend .shadowbox;
padding: 0.5ch;
background-color: var(--clr-bg-ui);
display: grid;
grid-template-columns: max-content 0 auto max-content;
grid-gap: 0.5ch;
grid-auto-rows: minmax(1ch, auto);
width: 100%;
h1 {
margin: 0;
padding: 0.5ch;
font-size: 14pt;
word-break: keep-all;
background-color: var(--clr-bg-theme);
color: var(--clr-text-theme);
font-family: var(--font-family-heading);
font-weight: normal;
text-align: center;
display: flex;
justify-content: space-between;
}
#suggestions-anchor {
margin: -0.5ch; // We need this anchor for the typeahead suggestions, but we don't want it to affect the layout
padding: 0;
}
input[type="text"] {
font-family: monospace;
font-size: 12pt;
padding: 0.5ch;
border: 1px solid var(--clr-border);
background-color: inherit;
}
input[type="submit"] {
font-size: 12pt;
border: 1px solid var(--clr-border);
background-color: var(--clr-bg-ui);
cursor: pointer;
}
// white suggesitons looks fine in dark mode
.suggestions {
background-color: #fff;
padding: .5ch;
margin-top: 5.5ch;
margin-left: 1ch;
position: absolute;
display: inline-block;
width: 300px;
border-left: 1px solid #ccc;
border-top: 1px solid #ccc;
box-shadow: 5px 5px 5px var(--clr-shadow);
z-index: 10;
a {
display: block;
color: #000;
font-size: 12pt;
font-family: 'fixedsys', monospace, serif;
text-decoration: none;
outline: none;
}
a:focus {
display: block;
background-color: #000;
color: #eee;
}
}
}
.filter-toggle-on {
a:before {
content: '';
margin-right: 1.5ch;
}
}
.filter-toggle-off {
a:before {
content: '';
margin-right: 1.5ch;
}
}
#filters {
@extend .shadowbox;
margin-top: 1ch;
background-color: var(--clr-bg-ui);
h2 {
@extend .heading;
background-color: var(--clr-bg-theme);
}
h3 {
@extend .heading;
background-color: var(--clr-bg-highlight);
color: var(--clr-text-highlight);
font-family: sans-serif;
border-bottom: 1px solid #000;
}
hr {
border-top: 0.5px solid var(--clr-border);
border-bottom: none;
}
ul {
list-style-type: none;
padding-left: 0;
li {
padding: 1ch;
a {
color: inherit;
text-decoration: none;
}
a:hover, a:focus {
border-bottom: 1px solid var(--clr-bg-theme);
}
}
li.current {
border-left: 4px solid var(--clr-bg-theme);
background-color: var(--clr-bg-highlight);
a {
margin-left: -4px;
}
}
}
}
.search-result {
@extend .shadowbox;
margin: 1ch 0 2ch 0;
.url {
background-color: var(--clr-bg-theme);
padding-left: 0.5ch;
a {
word-break: break-all;
font-family: monospace;
font-size: 8pt;
color: var(--clr-text-theme);
text-shadow: 0 0 1ch #000; // guarantee decent contrast across background colors
}
a:visited {
color: var(--clr-heading-link-visited);
}
}
h2 {
a {
word-break: break-all;
color: var(--clr-text-ui);
text-decoration: none;
}
font-size: 12pt;
@extend .heading;
background-color:var(--clr-bg-highlight);
}
.description {
background-color: var(--clr-bg-ui);
word-break: break-word;
padding: 1ch;
margin: 0;
}
ul.additional-results {
background-color: var(--clr-bg-ui);
padding: 1ch;
list-style: none;
margin: 0;
a {
color: inherit;
}
}
}
.search-result[data-ms-rank="1"] { .url, h2 { filter: grayscale(0%); } }
.search-result[data-ms-rank="2"] { .url, h2 { filter: grayscale(5%); } }
.search-result[data-ms-rank="3"] { .url, h2 { filter: grayscale(15%); } }
.search-result[data-ms-rank="4"] { .url, h2 { filter: grayscale(20%); } }
.search-result[data-ms-rank="5"] { .url, h2 { filter: grayscale(30%); } }
.search-result[data-ms-rank="10"] { .url, h2 { filter: grayscale(60%); } }
.utils {
display: flex;
font-size: 10pt;
padding: 1ch;
background-color: var(--clr-bg-highlight);
> * {
margin-right: 1ch;
margin-left: 1ch;
}
.meta {
flex-grow: 2;
text-align: right;
}
.meta > * {
padding-left: 4px;
}
a {
color: var(--clr-text-highlight);
}
}
@media (max-device-width: 624px) {
[data-has-js="true"] body { // This property is set via js so we can selectively enable these changes only if JS is enabled;
// This is desirable since mobile navigation is JS-driven. If JS is disabled, having a squished
// GUI is better than having no working UI.
margin: 0 !important;
padding: 0 0 0 0 !important;
max-width: 100%;
#suggestions-anchor { display: none; } // suggestions are not useful on mobile
.sidebar-narrow {
display: block; // fix for bizarre chrome rendering issue
}
#mcfeast {
display: inline;
float: right;
width: 2rem;
font-size: 1rem;
}
#menu-close {
float: right;
display: inline;
}
#filters {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
margin: 0;
padding: 0;
z-index: 100;
}
.sidebar-narrow {
grid-template-columns: auto;
}
#search-box {
grid-template-columns: auto;
}
#filters {
margin-top: 0;
}
.search-result {
margin-left: 0;
margin-right: 0;
}
}
}
.page-link {
padding-top: 0.25ch;
padding-bottom: 0.25ch;
padding-left: 0.5ch;
padding-right: 0.5ch;
margin-right: 0.5ch;
font-size: 12pt;
border: 1px solid var(--clr-border);
background-color: var(--clr-bg-highlight);
color: var(--clr-text-ui) !important;
text-decoration: none;
}
.page-link.active {
border: 1px solid var(--clr-text-ui);
background-color: var(--clr-bg-ui);
}
// The search results page is very confusing on text-based browsers, so we add a hr to separate the search results. This is
// hidden on modern browsers via CSS.
hr.w3m-helper { display: none; }
// This is a screenreader-only class that hides content from visual browsers, but allows screenreaders and
// text-based browsers to access it.
.screenreader-only {
position:absolute;
left:-10000px;
top:auto;
width:1px;
height:1px;
overflow:hidden;
}

View File

@ -1,52 +0,0 @@
package nu.marginalia.search.command.commands;
import nu.marginalia.search.command.SearchParameters;
import nu.marginalia.search.exceptions.RedirectException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class BangCommandTest {
public BangCommand bangCommand = new BangCommand();
@Test
public void testG() {
try {
bangCommand.process(null,
new SearchParameters(null, " !g test",
null, null, null, null, null, false, 1)
);
Assertions.fail("Should have thrown RedirectException");
}
catch (RedirectException ex) {
assertEquals("https://www.google.com/search?q=test", ex.newUrl);
}
}
@Test
public void testMatchPattern() {
var match = bangCommand.matchBangPattern("!g test", "!g");
assertTrue(match.isPresent());
assertEquals(match.get(), "test");
}
@Test
public void testMatchPattern2() {
var match = bangCommand.matchBangPattern("test !g", "!g");
assertTrue(match.isPresent());
assertEquals(match.get(), "test");
}
@Test
public void testMatchPattern3() {
var match = bangCommand.matchBangPattern("hello !g world", "!g");
assertTrue(match.isPresent());
assertEquals(match.get(), "hello world");
}
}

View File

@ -3,7 +3,7 @@ package nu.marginalia.status;
import com.google.inject.Inject; import com.google.inject.Inject;
import nu.marginalia.renderer.RendererFactory; import nu.marginalia.renderer.RendererFactory;
import nu.marginalia.service.server.BaseServiceParams; import nu.marginalia.service.server.BaseServiceParams;
import nu.marginalia.service.server.Service; import nu.marginalia.service.server.SparkService;
import nu.marginalia.status.db.StatusMetricDb; import nu.marginalia.status.db.StatusMetricDb;
import nu.marginalia.status.endpoints.ApiEndpoint; import nu.marginalia.status.endpoints.ApiEndpoint;
import nu.marginalia.status.endpoints.MainSearchEndpoint; import nu.marginalia.status.endpoints.MainSearchEndpoint;
@ -19,7 +19,7 @@ import java.util.concurrent.TimeUnit;
import static spark.Spark.get; import static spark.Spark.get;
public class StatusService extends Service { public class StatusService extends SparkService {
private final ScheduledExecutorService scheduledExecutorService = private final ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(5); Executors.newScheduledThreadPool(5);

View File

@ -11,7 +11,7 @@ import nu.marginalia.rss.svc.FeedsGrpcService;
import nu.marginalia.screenshot.ScreenshotService; import nu.marginalia.screenshot.ScreenshotService;
import nu.marginalia.service.discovery.property.ServicePartition; import nu.marginalia.service.discovery.property.ServicePartition;
import nu.marginalia.service.server.BaseServiceParams; import nu.marginalia.service.server.BaseServiceParams;
import nu.marginalia.service.server.Service; import nu.marginalia.service.server.SparkService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import spark.Request; import spark.Request;
@ -20,7 +20,7 @@ import spark.Spark;
import java.util.List; import java.util.List;
public class AssistantService extends Service { public class AssistantService extends SparkService {
private final Logger logger = LoggerFactory.getLogger(getClass()); private final Logger logger = LoggerFactory.getLogger(getClass());
private final Gson gson = GsonFactory.get(); private final Gson gson = GsonFactory.get();
private final Suggestions suggestions; private final Suggestions suggestions;

View File

@ -12,7 +12,7 @@ import nu.marginalia.model.gson.GsonFactory;
import nu.marginalia.screenshot.ScreenshotService; import nu.marginalia.screenshot.ScreenshotService;
import nu.marginalia.service.ServiceMonitors; import nu.marginalia.service.ServiceMonitors;
import nu.marginalia.service.server.BaseServiceParams; import nu.marginalia.service.server.BaseServiceParams;
import nu.marginalia.service.server.Service; import nu.marginalia.service.server.SparkService;
import nu.marginalia.service.server.StaticResources; import nu.marginalia.service.server.StaticResources;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -22,7 +22,7 @@ import spark.Spark;
import java.util.Map; import java.util.Map;
public class ControlService extends Service { public class ControlService extends SparkService {
private final Logger logger = LoggerFactory.getLogger(getClass()); private final Logger logger = LoggerFactory.getLogger(getClass());
private final Gson gson = GsonFactory.get(); private final Gson gson = GsonFactory.get();

View File

@ -4,7 +4,7 @@ import com.google.inject.Inject;
import nu.marginalia.execution.*; import nu.marginalia.execution.*;
import nu.marginalia.service.discovery.property.ServicePartition; import nu.marginalia.service.discovery.property.ServicePartition;
import nu.marginalia.service.server.BaseServiceParams; import nu.marginalia.service.server.BaseServiceParams;
import nu.marginalia.service.server.Service; import nu.marginalia.service.server.SparkService;
import nu.marginalia.service.server.mq.MqRequest; import nu.marginalia.service.server.mq.MqRequest;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -13,7 +13,7 @@ import spark.Spark;
import java.util.List; import java.util.List;
// Weird name for this one to not have clashes with java.util.concurrent.ExecutorService // Weird name for this one to not have clashes with java.util.concurrent.ExecutorService
public class ExecutorSvc extends Service { public class ExecutorSvc extends SparkService {
private static final Logger logger = LoggerFactory.getLogger(ExecutorSvc.class); private static final Logger logger = LoggerFactory.getLogger(ExecutorSvc.class);
private final ExecutionInit executionInit; private final ExecutionInit executionInit;

View File

@ -11,7 +11,7 @@ import nu.marginalia.service.control.ServiceEventLog;
import nu.marginalia.service.discovery.property.ServicePartition; import nu.marginalia.service.discovery.property.ServicePartition;
import nu.marginalia.service.server.BaseServiceParams; import nu.marginalia.service.server.BaseServiceParams;
import nu.marginalia.service.server.Initialization; import nu.marginalia.service.server.Initialization;
import nu.marginalia.service.server.Service; import nu.marginalia.service.server.SparkService;
import nu.marginalia.service.server.mq.MqRequest; import nu.marginalia.service.server.mq.MqRequest;
import nu.marginalia.storage.FileStorageService; import nu.marginalia.storage.FileStorageService;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -25,7 +25,7 @@ import java.util.List;
import static nu.marginalia.linkdb.LinkdbFileNames.DOCDB_FILE_NAME; import static nu.marginalia.linkdb.LinkdbFileNames.DOCDB_FILE_NAME;
import static nu.marginalia.linkdb.LinkdbFileNames.DOMAIN_LINKS_FILE_NAME; import static nu.marginalia.linkdb.LinkdbFileNames.DOMAIN_LINKS_FILE_NAME;
public class IndexService extends Service { public class IndexService extends SparkService {
private final Logger logger = LoggerFactory.getLogger(getClass()); private final Logger logger = LoggerFactory.getLogger(getClass());
@NotNull @NotNull

View File

@ -5,7 +5,7 @@ import nu.marginalia.functions.searchquery.QueryGRPCService;
import nu.marginalia.linkgraph.AggregateLinkGraphService; import nu.marginalia.linkgraph.AggregateLinkGraphService;
import nu.marginalia.service.discovery.property.ServicePartition; import nu.marginalia.service.discovery.property.ServicePartition;
import nu.marginalia.service.server.BaseServiceParams; import nu.marginalia.service.server.BaseServiceParams;
import nu.marginalia.service.server.Service; import nu.marginalia.service.server.SparkService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import spark.Spark; import spark.Spark;
@ -13,7 +13,7 @@ import spark.Spark;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
public class QueryService extends Service { public class QueryService extends SparkService {
private static final Logger logger = LoggerFactory.getLogger(QueryService.class); private static final Logger logger = LoggerFactory.getLogger(QueryService.class);

View File

@ -101,6 +101,10 @@ include 'third-party:commons-codec'
include 'third-party:parquet-floor' include 'third-party:parquet-floor'
include 'third-party:encyclopedia-marginalia-nu' include 'third-party:encyclopedia-marginalia-nu'
ext {
joobyVersion = '3.5.5'
}
dependencyResolutionManagement { dependencyResolutionManagement {
repositories { repositories {
@ -230,6 +234,10 @@ dependencyResolutionManagement {
library('jetty-util','org.eclipse.jetty','jetty-util').version('9.4.54.v20240208') library('jetty-util','org.eclipse.jetty','jetty-util').version('9.4.54.v20240208')
library('jetty-servlet','org.eclipse.jetty','jetty-servlet').version('9.4.54.v20240208') library('jetty-servlet','org.eclipse.jetty','jetty-servlet').version('9.4.54.v20240208')
library('jooby-netty','io.jooby','jooby-netty').version(joobyVersion)
library('jooby-jte','io.jooby','jooby-jte').version(joobyVersion)
library('jooby-apt','io.jooby','jooby-apt').version(joobyVersion)
library('jte','gg.jte','jte').version('3.1.15') library('jte','gg.jte','jte').version('3.1.15')
library('slop', 'nu.marginalia', 'slop').version('0.0.8-SNAPSHOT') library('slop', 'nu.marginalia', 'slop').version('0.0.8-SNAPSHOT')
@ -250,7 +258,7 @@ dependencyResolutionManagement {
bundle('parquet', ['parquet-column', 'parquet-hadoop']) bundle('parquet', ['parquet-column', 'parquet-hadoop'])
bundle('junit', ['junit.jupiter', 'junit.jupiter.engine']) bundle('junit', ['junit.jupiter', 'junit.jupiter.engine'])
bundle('flyway', ['flyway.core', 'flyway.mysql']) bundle('flyway', ['flyway.core', 'flyway.mysql'])
bundle('jooby', ['jooby-netty', 'jooby-jte'])
bundle('curator', ['curator-framework', 'curator-x-discovery']) bundle('curator', ['curator-framework', 'curator-x-discovery'])