(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'
dockerImageRegistry='marginalia'
jibVersion = '3.4.3'
}
idea {

View File

@ -42,6 +42,12 @@ dependencies {
implementation libs.bundles.curator
implementation libs.bundles.flyway
libs.bundles.jooby.get().each {
implementation dependencies.create(it) {
exclude group: 'org.slf4j'
}
}
testImplementation libs.bundles.slf4j.test
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;
public class Service {
public class SparkService {
private final Logger logger = LoggerFactory.getLogger(getClass());
// Marker for filtering out sensitive content from the persistent logs
@ -43,10 +43,10 @@ public class Service {
private final int node;
private GrpcServer grpcServer;
public Service(BaseServiceParams params,
Runnable configureStaticFiles,
ServicePartition partition,
List<DiscoverableService> grpcServices) throws Exception {
public SparkService(BaseServiceParams params,
Runnable configureStaticFiles,
ServicePartition partition,
List<DiscoverableService> grpcServices) throws Exception {
this.initialization = params.initialization;
var config = params.configuration;
@ -126,18 +126,18 @@ public class Service {
}
}
public Service(BaseServiceParams params,
ServicePartition partition,
List<DiscoverableService> grpcServices) throws Exception {
public SparkService(BaseServiceParams params,
ServicePartition partition,
List<DiscoverableService> grpcServices) throws Exception {
this(params,
Service::defaultSparkConfig,
SparkService::defaultSparkConfig,
partition,
grpcServices);
}
public Service(BaseServiceParams params) throws Exception {
public SparkService(BaseServiceParams params) throws Exception {
this(params,
Service::defaultSparkConfig,
SparkService::defaultSparkConfig,
ServicePartition.any(),
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.inbox.MqInboxResponse;
import nu.marginalia.mq.inbox.MqSubscription;
import nu.marginalia.service.server.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -15,10 +14,10 @@ import java.util.Map;
public class ServiceMqSubscription implements MqSubscription {
private static final Logger logger = LoggerFactory.getLogger(ServiceMqSubscription.class);
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;
/* 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.model.gson.GsonFactory;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -21,7 +21,7 @@ import spark.Request;
import spark.Response;
import spark.Spark;
public class ApiService extends Service {
public class ApiService extends SparkService {
private final Logger logger = LoggerFactory.getLogger(getClass());
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.screenshot.ScreenshotService;
import nu.marginalia.service.server.BaseServiceParams;
import nu.marginalia.service.server.Service;
import nu.marginalia.service.server.SparkService;
import org.jetbrains.annotations.NotNull;
import spark.Request;
import spark.Response;
@ -18,7 +18,7 @@ import spark.Spark;
import java.util.Map;
import java.util.Optional;
public class DatingService extends Service {
public class DatingService extends SparkService {
private final DomainBlacklist blacklist;
private final DbBrowseDomainsSimilarCosine browseSimilarCosine;
private final DbBrowseDomainsRandom browseRandom;

View File

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

View File

@ -1,10 +1,8 @@
plugins {
id 'java'
id 'io.freefair.sass-base' version '8.4'
id 'io.freefair.sass-java' version '8.4'
id 'application'
id 'jvm-test-suite'
id 'gg.jte.gradle' version '3.1.15'
id 'com.google.cloud.tools.jib' version '3.4.3'
}
@ -15,18 +13,16 @@ application {
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 {
toolchain {
languageVersion.set(JavaLanguageVersion.of(rootProject.ext.jvmVersion))
}
}
sass {
sourceMapEnabled = true
sourceMapEmbed = true
outputStyle = EXPANDED
}
apply from: "$rootProject.projectDir/srcsets.gradle"
apply from: "$rootProject.projectDir/docker.gradle"
@ -64,18 +60,24 @@ dependencies {
exclude group: 'com.google.guava'
}
implementation libs.handlebars
implementation dependencies.create(libs.spark.get()) {
exclude group: 'org.eclipse.jetty'
}
implementation libs.bundles.jetty
implementation libs.opencsv
implementation libs.trove
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.bundles.gson
implementation libs.bundles.mariadb
implementation libs.bundles.nlp
annotationProcessor libs.jooby.apt
testImplementation libs.bundles.slf4j.test
testImplementation libs.bundles.junit
testImplementation libs.mockito
@ -85,12 +87,14 @@ dependencies {
testImplementation 'org.testcontainers:mariadb:1.17.4'
testImplementation 'org.testcontainers:junit-jupiter:1.17.4'
testImplementation project(':code:libraries:test-helpers')
testImplementation dependencies.create(libs.spark.get())
}
task compileTailwind {
def inputFile = file('tailwind/globals.css')
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')
inputs.file inputFile

View File

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

View File

@ -5,20 +5,16 @@ import io.prometheus.client.Counter;
import io.prometheus.client.Histogram;
import nu.marginalia.WebsiteUrl;
import nu.marginalia.search.svc.*;
import nu.marginalia.service.discovery.property.ServicePartition;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import spark.Route;
import spark.Spark;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class SearchService extends Service {
public class SearchService extends JoobyService {
private final WebsiteUrl websiteUrl;
private final StaticResources staticResources;
@ -41,97 +37,103 @@ public class SearchService extends Service {
WebsiteUrl websiteUrl,
StaticResources staticResources,
SearchFrontPageService frontPageService,
SearchErrorPageService errorPageService,
SearchAddToCrawlQueueService addToCrawlQueueService,
SearchSiteInfoService siteInfoService,
SearchCrosstalkService crosstalkService,
SearchBrowseService searchBrowseService,
SearchQueryService searchQueryService)
throws Exception
{
super(params);
throws Exception {
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.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 */
private static class SearchServiceMetrics implements Route {
private final Route delegatedRoute;
static void get(String path, Route route) {
Spark.get(path, new SearchServiceMetrics(route));
}
static void post(String path, Route route) {
Spark.post(path, new SearchServiceMetrics(route));
}
private SearchServiceMetrics(Route delegatedRoute) {
this.delegatedRoute = delegatedRoute;
}
@Override
public Object handle(Request request, Response response) throws Exception {
return wmsa_search_service_request_time
.labels(request.matchedPath(), request.requestMethod())
.time(() -> delegatedRoute.handle(request, response));
}
}
private Object serveStatic(Request request, Response response) {
String resource = request.params("resource");
staticResources.serveStatic("search", resource, request, response);
return "";
}
private Object siteSearchRedir(Request request, Response response) {
final String site = request.params("site");
final String searchTerms;
if (request.splat().length == 0) searchTerms = "";
else searchTerms = request.splat()[0];
final String query = URLEncoder.encode(String.format("%s site:%s", searchTerms, site), StandardCharsets.UTF_8).trim();
final String profile = request.queryParamOrDefault("profile", "yolo");
response.redirect(websiteUrl.withPath("search?query="+query+"&profile="+profile));
return "";
}
//
//
// /** Wraps a route with a timer and a counter */
// private static class SearchServiceMetrics implements Route {
// private final Route delegatedRoute;
//
// static void get(String path, Route route) {
// Spark.get(path, new SearchServiceMetrics(route));
// }
// static void post(String path, Route route) {
// Spark.post(path, new SearchServiceMetrics(route));
// }
//
// private SearchServiceMetrics(Route delegatedRoute) {
// this.delegatedRoute = delegatedRoute;
// }
//
// @Override
// public Object handle(Request request, Response response) throws Exception {
// return wmsa_search_service_request_time
// .labels(request.matchedPath(), request.requestMethod())
// .time(() -> delegatedRoute.handle(request, response));
// }
// }
//
// private Object serveStatic(Request request, Response response) {
// String resource = request.params("resource");
// staticResources.serveStatic("search", resource, request, response);
// return "";
// }
//
// private Object siteSearchRedir(Request request, Response response) {
// final String site = request.params("site");
// final String searchTerms;
//
// if (request.splat().length == 0) searchTerms = "";
// else searchTerms = request.splat()[0];
//
// final String query = URLEncoder.encode(String.format("%s site:%s", searchTerms, site), StandardCharsets.UTF_8).trim();
// final String profile = request.queryParamOrDefault("profile", "yolo");
//
// response.redirect(websiteUrl.withPath("search?query="+query+"&profile="+profile));
//
// return "";
// }
}

View File

@ -1,8 +1,8 @@
package nu.marginalia.search.command;
import com.google.inject.Inject;
import io.jooby.ModelAndView;
import nu.marginalia.search.command.commands.*;
import spark.Response;
import java.util.ArrayList;
import java.util.List;
@ -30,14 +30,14 @@ public class CommandEvaluator {
defaultCommand = search;
}
public Object eval(Response response, SearchParameters parameters) {
public ModelAndView<?> eval(SearchParameters parameters) throws Exception {
for (var cmd : specialCommands) {
var maybe = cmd.process(response, parameters);
var maybe = cmd.process(parameters);
if (maybe.isPresent())
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;
import spark.Response;
import io.jooby.ModelAndView;
import java.util.Optional;
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.model.EdgeDomain;
import nu.marginalia.search.model.SearchProfile;
import spark.Request;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.StringJoiner;
import static nu.marginalia.search.command.SearchRecentParameter.RECENT;
@ -38,19 +36,6 @@ public record SearchParameters(WebsiteUrl url,
false,
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() {
return profile.filterId;

View File

@ -1,10 +1,10 @@
package nu.marginalia.search.command.commands;
import com.google.inject.Inject;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import nu.marginalia.search.command.SearchCommandInterface;
import nu.marginalia.search.command.SearchParameters;
import nu.marginalia.search.exceptions.RedirectException;
import spark.Response;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@ -24,7 +24,7 @@ public class BangCommand implements SearchCommandInterface {
}
@Override
public Optional<Object> process(Response response, SearchParameters parameters) {
public Optional<ModelAndView<?>> process(SearchParameters parameters) {
for (var entry : bangsToPattern.entrySet()) {
String bangPattern = entry.getKey();
@ -34,7 +34,7 @@ public class BangCommand implements SearchCommandInterface {
if (match.isPresent()) {
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;
import com.google.inject.Inject;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import nu.marginalia.search.command.SearchCommandInterface;
import nu.marginalia.search.command.SearchParameters;
import spark.Response;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Pattern;
@ -19,7 +21,7 @@ public class BrowseRedirectCommand implements SearchCommandInterface {
}
@Override
public Optional<Object> process(Response response, SearchParameters parameters) {
public Optional<ModelAndView<?>> process(SearchParameters parameters) {
if (!queryPatternPredicate.test(parameters.query())) {
return Optional.empty();
}
@ -35,13 +37,9 @@ public class BrowseRedirectCommand implements SearchCommandInterface {
redirectPath = "/explore/" + word;
}
return Optional.of("""
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0; %s">
""".formatted(redirectPath));
return Optional.of(
new MapModelAndView("/redirect.jte", Map.of("url", redirectPath))
);
}

View File

@ -1,12 +1,13 @@
package nu.marginalia.search.command.commands;
import com.google.inject.Inject;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import nu.marginalia.search.JteRenderer;
import nu.marginalia.search.command.SearchCommandInterface;
import nu.marginalia.search.command.SearchParameters;
import nu.marginalia.search.model.NavbarModel;
import nu.marginalia.search.svc.SearchUnitConversionService;
import spark.Response;
import java.util.Map;
import java.util.Optional;
@ -24,9 +25,10 @@ public class ConvertCommand implements SearchCommandInterface {
}
@Override
public Optional<Object> process(Response response, SearchParameters parameters) {
public Optional<ModelAndView<?>> process(SearchParameters parameters) {
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,
"navbar", NavbarModel.SEARCH,
"result", s)

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -1,41 +1,28 @@
package nu.marginalia.search.svc;
import com.google.inject.Inject;
import nu.marginalia.WebsiteUrl;
import nu.marginalia.search.JteRenderer;
import io.jooby.MapModelAndView;
import io.jooby.ModelAndView;
import nu.marginalia.search.command.SearchParameters;
import nu.marginalia.search.model.NavbarModel;
import nu.marginalia.search.model.SearchErrorMessageModel;
import nu.marginalia.search.model.SearchFilters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import java.io.IOException;
import java.util.Map;
public class SearchErrorPageService {
private final WebsiteUrl websiteUrl;
private final JteRenderer jteRenderer;
private final Logger logger = LoggerFactory.getLogger(getClass());
@Inject
public SearchErrorPageService(WebsiteUrl websiteUrl,
JteRenderer jteRenderer) throws IOException {
this.websiteUrl = websiteUrl;
this.jteRenderer = jteRenderer;
public SearchErrorPageService() throws IOException {
}
public void serveError(Request request, Response rsp) {
public ModelAndView<?> serveError(SearchParameters parameters) {
var params = SearchParameters.forRequest(
request.queryParamOrDefault("query", ""),
websiteUrl,
request);
rsp.body(jteRenderer.render("serp/error.jte",
return new MapModelAndView("serp/error.jte",
Map.of("navbar", NavbarModel.LIMBO,
"model", new SearchErrorMessageModel(
"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
to reload from a cold restart. Thanks for your patience.
""",
params,
new SearchFilters(params)
parameters,
new SearchFilters(parameters)
)
)
));
);
}
}

View File

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

View File

@ -1,14 +1,17 @@
package nu.marginalia.search.svc;
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.search.command.CommandEvaluator;
import nu.marginalia.search.command.SearchParameters;
import nu.marginalia.search.exceptions.RedirectException;
import nu.marginalia.search.command.*;
import nu.marginalia.search.model.SearchProfile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.Request;
import spark.Response;
import java.util.Objects;
public class SearchQueryService {
@ -27,36 +30,34 @@ public class SearchQueryService {
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 {
return searchCommandEvaulator.eval(response, parseParameters(request));
}
catch (RedirectException ex) {
response.redirect(ex.newUrl);
SearchParameters parameters = new SearchParameters(websiteUrl,
query,
SearchProfile.getSearchProfile(profile),
SearchJsParameter.parse(js),
SearchRecentParameter.parse(recent),
SearchTitleParameter.parse(title),
SearchAdtechParameter.parse(adtech),
false,
Objects.requireNonNullElse(page,1));
return searchCommandEvaulator.eval(parameters);
}
catch (Exception ex) {
logger.error("Error", ex);
errorPageService.serveError(request, response);
}
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());
return errorPageService.serveError(SearchParameters.defaultsForQuery(websiteUrl, query, page));
}
}
}

View File

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

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 WebsiteUrl websiteUrl
<!DOCTYPE html>
<html lang="en">

View File

@ -1,3 +1,4 @@
@import nu.marginalia.search.svc.SearchFlagSiteService
@import nu.marginalia.search.svc.SearchSiteInfoService.*
@param ReportDomain reportDomain
@ -14,7 +15,6 @@
instead of using this form.
</div>
@else
<form class="space-y-6 p-4" method="post">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">
@ -22,10 +22,9 @@
</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">
<option value="">Select issue type...</option>
<option value="spam">Spam</option>
<option value="dead-link">Dead Link</option>
<option value="inappropriate">Inappropriate Content</option>
<option value="other">Other</option>
@for (SearchFlagSiteService.CategoryItem item : SearchFlagSiteService.categories)
<option value="${item.categoryName()}">${item.categoryDesc()}</option>
@endfor
</select>
</div>

View File

@ -570,6 +570,10 @@ video {
visibility: visible;
}
.static {
position: static;
}
.fixed {
position: fixed;
}
@ -586,12 +590,8 @@ video {
inset: 0px;
}
.bottom-0 {
bottom: 0px;
}
.left-0 {
left: 0px;
.bottom-10 {
bottom: 2.5rem;
}
.right-5 {
@ -602,12 +602,12 @@ video {
top: 0px;
}
.top-4 {
top: 1rem;
.top-2 {
top: 0.5rem;
}
.top-5 {
top: 1.25rem;
.top-4 {
top: 1rem;
}
.z-50 {
@ -725,6 +725,10 @@ video {
margin-top: 0.25rem;
}
.mt-10 {
margin-top: 2.5rem;
}
.mt-2 {
margin-top: 0.5rem;
}
@ -789,6 +793,10 @@ video {
width: 16rem;
}
.w-96 {
width: 24rem;
}
.w-full {
width: 100%;
}
@ -829,6 +837,10 @@ video {
flex-grow: 1;
}
.basis-1\/2 {
flex-basis: 50%;
}
.cursor-pointer {
cursor: pointer;
}
@ -901,6 +913,12 @@ video {
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]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.25rem * var(--tw-space-x-reverse));
@ -1001,6 +1019,10 @@ video {
border-radius: 9999px;
}
.rounded-lg {
border-radius: 0.5rem;
}
.rounded-md {
border-radius: 0.375rem;
}
@ -1009,6 +1031,10 @@ video {
border-radius: 0.125rem;
}
.rounded-xl {
border-radius: 0.75rem;
}
.border {
border-width: 1px;
}
@ -1017,6 +1043,10 @@ video {
border-bottom-width: 1px;
}
.border-none {
border-style: none;
}
.border-blue-200 {
--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));
}
.bg-gray-50\/90 {
background-color: rgb(249 250 251 / 0.9);
}
.bg-green-50 {
--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);
}
.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 {
--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);
@ -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);
}
.outline-1 {
outline-width: 1px;
}
.outline-margeblue {
outline-color: #3e5f6f;
}
.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);
}
@ -1564,6 +1604,15 @@ video {
--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) {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
@ -1590,10 +1639,6 @@ video {
}
@media (min-width: 640px) {
.sm\:static {
position: static;
}
.sm\:m-0 {
margin: 0px;
}
@ -1634,21 +1679,14 @@ video {
margin-bottom: calc(0px * var(--tw-space-y-reverse));
}
.sm\:border-none {
border-style: none;
.sm\:space-y-6 > :not([hidden]) ~ :not([hidden]) {
--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 {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
}
.sm\:p-0 {
padding: 0px;
}
.sm\:p-3 {
padding: 0.75rem;
.sm\:p-4 {
padding: 1rem;
}
.sm\:px-2 {
@ -1673,6 +1711,14 @@ video {
margin-bottom: 1rem;
}
.md\:ml-16 {
margin-left: 4rem;
}
.md\:mr-16 {
margin-right: 4rem;
}
.md\:mr-8 {
margin-right: 2rem;
}
@ -1685,6 +1731,14 @@ video {
display: inline;
}
.md\:hidden {
display: none;
}
.md\:w-32 {
width: 8rem;
}
.md\:w-64 {
width: 16rem;
}
@ -1709,6 +1763,10 @@ video {
place-items: start;
}
.md\:place-items-baseline {
place-items: baseline;
}
.md\:gap-8 {
gap: 2rem;
}
@ -1725,6 +1783,10 @@ video {
margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
}
.md\:p-4 {
padding: 1rem;
}
.md\:px-4 {
padding-left: 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 nu.marginalia.renderer.RendererFactory;
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.endpoints.ApiEndpoint;
import nu.marginalia.status.endpoints.MainSearchEndpoint;
@ -19,7 +19,7 @@ import java.util.concurrent.TimeUnit;
import static spark.Spark.get;
public class StatusService extends Service {
public class StatusService extends SparkService {
private final ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(5);

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import com.google.inject.Inject;
import nu.marginalia.execution.*;
import nu.marginalia.service.discovery.property.ServicePartition;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -13,7 +13,7 @@ import spark.Spark;
import java.util.List;
// 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 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.server.BaseServiceParams;
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.storage.FileStorageService;
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.DOMAIN_LINKS_FILE_NAME;
public class IndexService extends Service {
public class IndexService extends SparkService {
private final Logger logger = LoggerFactory.getLogger(getClass());
@NotNull

View File

@ -5,7 +5,7 @@ import nu.marginalia.functions.searchquery.QueryGRPCService;
import nu.marginalia.linkgraph.AggregateLinkGraphService;
import nu.marginalia.service.discovery.property.ServicePartition;
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.LoggerFactory;
import spark.Spark;
@ -13,7 +13,7 @@ import spark.Spark;
import java.io.IOException;
import java.util.List;
public class QueryService extends Service {
public class QueryService extends SparkService {
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:encyclopedia-marginalia-nu'
ext {
joobyVersion = '3.5.5'
}
dependencyResolutionManagement {
repositories {
@ -230,6 +234,10 @@ dependencyResolutionManagement {
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('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('slop', 'nu.marginalia', 'slop').version('0.0.8-SNAPSHOT')
@ -250,7 +258,7 @@ dependencyResolutionManagement {
bundle('parquet', ['parquet-column', 'parquet-hadoop'])
bundle('junit', ['junit.jupiter', 'junit.jupiter.engine'])
bundle('flyway', ['flyway.core', 'flyway.mysql'])
bundle('jooby', ['jooby-netty', 'jooby-jte'])
bundle('curator', ['curator-framework', 'curator-x-discovery'])