Rdf4jServerWorkbenchApplication.java

/*******************************************************************************
 * Copyright (c) 2025 Eclipse RDF4J contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
// Some portions generated by Codex
package org.eclipse.rdf4j.tools.serverboot;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.MultipartConfigElement;

import org.apache.catalina.Context;
import org.eclipse.rdf4j.common.platform.Platform;
import org.eclipse.rdf4j.common.platform.PlatformFactory;
import org.eclipse.rdf4j.workbench.proxy.CacheFilter;
import org.eclipse.rdf4j.workbench.proxy.CookieCacheControlFilter;
import org.eclipse.rdf4j.workbench.proxy.RedirectFilter;
import org.eclipse.rdf4j.workbench.proxy.WorkbenchGateway;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.tuckey.web.filters.urlrewrite.UrlRewriteFilter;

import com.github.ziplet.filter.compression.CompressingFilter;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;

@SpringBootApplication
public class Rdf4jServerWorkbenchApplication {

	private static final Logger logger = LoggerFactory.getLogger(Rdf4jServerWorkbenchApplication.class);
	private static final String APP_DATA_BASEDIR_PROPERTY = Platform.APPDATA_BASEDIR_PROPERTY;
	private static final String[] APPLICATION_IDS = { "Server", "webapp-base" };

	public static void main(String[] args) {
		ensureAppDataDirAccessible();
		SpringApplication application = new SpringApplication(Rdf4jServerWorkbenchApplication.class);
		SignalShutdownHandler signalShutdownHandler = SignalShutdownHandler.register("INT", "TERM");
		ConfigurableApplicationContext context = application.run(args);
		signalShutdownHandler.attachContext(context);
	}

	static void ensureAppDataDirAccessible() {
		if (System.getProperty(APP_DATA_BASEDIR_PROPERTY) != null) {
			return;
		}
		boolean defaultWritable = Arrays.stream(APPLICATION_IDS)
				.map(appId -> PlatformFactory.getPlatform().getApplicationDataDir(appId).toPath())
				.allMatch(Rdf4jServerWorkbenchApplication::ensureWritableDirectory);
		if (defaultWritable) {
			return;
		}

		Path fallback = Paths.get(System.getProperty("user.dir"), "target", "rdf4j-appdata").toAbsolutePath();
		boolean fallbackWritable = Arrays.stream(APPLICATION_IDS)
				.map(appId -> fallback.resolve(
						PlatformFactory.getPlatform().getRelativeApplicationDataDir(appId)))
				.allMatch(Rdf4jServerWorkbenchApplication::ensureWritableDirectory);

		if (!fallbackWritable) {
			throw new IllegalStateException(
					"Unable to create writable RDF4J application data directory at " + fallback);
		}

		System.setProperty(APP_DATA_BASEDIR_PROPERTY, fallback.toString());
		logger.warn("Using fallback RDF4J application data directory at {}", fallback);
	}

	private static boolean ensureWritableDirectory(Path directory) {
		try {
			Files.createDirectories(directory);
			Path probe = Files.createTempFile(directory, "rdf4j", ".tmp");
			Files.deleteIfExists(probe);
			return true;
		} catch (IOException e) {
			logger.debug("Unable to prepare RDF4J application data directory {}", directory, e);
			return false;
		}
	}

	@Bean(destroyMethod = "close")
	WebappResourceExtractor webappResourceExtractor() {
		return new WebappResourceExtractor();
	}

	@Bean
	TomcatServletWebServerFactory tomcatFactory(WebappResourceExtractor extractor) {
		TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
		factory.addContextCustomizers(workbenchResourcesCustomizer(extractor));
		return factory;
	}

	private TomcatContextCustomizer workbenchResourcesCustomizer(WebappResourceExtractor extractor) {
		return (Context context) -> context.setDocBase(extractor.getServerDocBase().toFile().getAbsolutePath());
	}

	@Bean
	ServletRegistrationBean<DispatcherServlet> rdf4jServerServlet(ApplicationContext parentContext) {
		DispatcherServlet dispatcherServlet = new DispatcherServlet();
		dispatcherServlet.setContextClass(ServerXmlWebApplicationContext.class);
		dispatcherServlet.setContextConfigLocation(String.join(",",
				"classpath:/rdf4j/server-webapp/WEB-INF/common-webapp-servlet.xml",
				"classpath:/rdf4j/server-webapp/WEB-INF/common-webapp-system-servlet.xml",
				"classpath:/rdf4j/server-webapp/WEB-INF/rdf4j-http-server-servlet.xml"));
		ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean<>(dispatcherServlet,
				serverServletUrlMappings().toArray(new String[0]));
		registration.setName("rdf4jServer");
		registration.setLoadOnStartup(1);
		return registration;
	}

	@Bean
	InitializingBean appDataDirInitializer() {
		return Rdf4jServerWorkbenchApplication::ensureAppDataDirAccessible;
	}

	@Bean
	ApplicationRunner consoleAppenderInitializer() {
		return args -> {
			if (!(LoggerFactory.getILoggerFactory() instanceof LoggerContext)) {
				return;
			}
			LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
			if (context.getLogger(Logger.ROOT_LOGGER_NAME).getAppender("Console") != null) {
				return;
			}

			PatternLayoutEncoder encoder = new PatternLayoutEncoder();
			encoder.setContext(context);
			encoder.setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger - %msg%n");
			encoder.start();

			ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
			appender.setContext(context);
			appender.setName("Console");
			appender.setEncoder(encoder);
			appender.start();

			context.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(appender);
		};
	}

	@Bean
	ServletRegistrationBean<WorkbenchGateway> rdf4jWorkbenchServlet() {
		WorkbenchGateway servlet = new WorkbenchGateway();
		ServletRegistrationBean<WorkbenchGateway> registration = new ServletRegistrationBean<>(servlet,
				workbenchServletUrlMappings().toArray(new String[0]));
		registration.setName("rdf4jWorkbench");
		registration.setLoadOnStartup(2);
		registration.setInitParameters(workbenchInitParameters());
		registration.setMultipartConfig(new MultipartConfigElement(""));
		return registration;
	}

	@Bean
	FilterRegistrationBean<ServerPrefixForwardFilter> serverPrefixForwardFilter() {
		FilterRegistrationBean<ServerPrefixForwardFilter> registration = new FilterRegistrationBean<>(
				new ServerPrefixForwardFilter());
		registration.addUrlPatterns("/rdf4j-server", "/rdf4j-server/*", "/rdf4j-workbench", "/rdf4j-workbench/*");
		registration.setName("ServerPrefixForwardFilter");
		registration.setOrder(1000);
		return registration;
	}

	@Bean
	FilterRegistrationBean<ServerRootDummyPageFilter> serverRootDummyPageFilter() {
		FilterRegistrationBean<ServerRootDummyPageFilter> registration = new FilterRegistrationBean<>(
				new ServerRootDummyPageFilter());
		registration.addUrlPatterns("/rdf4j-server/");
		registration.setName("serverRootDummyPage");
		registration.setOrder(-12);
		return registration;
	}

	private List<String> serverServletUrlMappings() {
		return WebXmlServletMappingExtractor.extractMappings(
				"rdf4j/server-webapp/WEB-INF/web.xml", "rdf4j-http-server", "/rdf4j-server", true);
	}

	private List<String> workbenchServletUrlMappings() {
		return WebXmlServletMappingExtractor.extractMappings(
				"rdf4j/workbench-webapp/WEB-INF/web.xml", "workbench", "/rdf4j-workbench", false);
	}

	@Bean
	FilterRegistrationBean<CompressingFilter> compressingFilter() {
		FilterRegistrationBean<CompressingFilter> registration = new FilterRegistrationBean<>(new CompressingFilter());
		registration.addUrlPatterns("/rdf4j-server/*");
		registration.setName("CompressingFilter");
		registration.setOrder(-10);
		registration.addInitParameter("excludeContentTypes",
				"application/x-binary-rdf,application/x-binary-rdf-results-table");
		return registration;
	}

	@Bean
	FilterRegistrationBean<UrlRewriteFilter> urlRewriteFilter() {
		FilterRegistrationBean<UrlRewriteFilter> registration = new FilterRegistrationBean<>(new UrlRewriteFilter());
		registration.addUrlPatterns("/rdf4j-server", "/rdf4j-server/");
		registration.setName("UrlRewriteFilter");
		registration.setOrder(-9);
		registration.addInitParameter("logLevel", "commons");
		registration.addInitParameter("statusEnabled", "false");
		return registration;
	}

	@Bean
	FilterRegistrationBean<ErrorLoggingFilter> errorLoggingFilter() {
		FilterRegistrationBean<ErrorLoggingFilter> registration = new FilterRegistrationBean<>(
				new ErrorLoggingFilter());
		registration.addUrlPatterns("/*");
		registration.setName("errorLoggingFilter");
		registration.setOrder(Integer.MAX_VALUE);
		return registration;
	}

	@Bean
	FilterRegistrationBean<CssPathFilter> pathFilter() {
		FilterRegistrationBean<CssPathFilter> registration = new FilterRegistrationBean<>(new CssPathFilter());
		registration.addUrlPatterns("*.css");
		registration.setName("PathFilter");
		registration.setOrder(-8);
		return registration;
	}

	@Bean
	FilterRegistrationBean<RedirectFilter> workbenchRedirectFilter() {
		FilterRegistrationBean<RedirectFilter> registration = new FilterRegistrationBean<>(new RedirectFilter());
		registration.addUrlPatterns("/rdf4j-workbench", "/rdf4j-workbench/*");
		registration.setName("redirect");
		registration.setOrder(-11);
		registration.addInitParameter("/", "/rdf4j-workbench/repositories");
		registration.addInitParameter("/rdf4j-workbench", "/rdf4j-workbench/repositories");
		registration.addInitParameter("/rdf4j-workbench/", "/rdf4j-workbench/repositories");
		return registration;
	}

	private Map<String, String> workbenchInitParameters() {
		Map<String, String> params = new LinkedHashMap<>();
		params.put("transformations", "/rdf4j-workbench/transformations");
		params.put("default-server", "/rdf4j-server");
		params.put("accepted-server-prefixes", "file: http: https:");
		params.put("change-server-path", "/NONE/server");
		params.put("cookie-max-age", "2592000");
		params.put("no-repository-id", "NONE");
		params.put("default-path", "/NONE/repositories");
		params.put("default-command", "/summary");
		params.put("default-limit", "100");
		params.put("default-queryLn", "SPARQL");
		params.put("default-infer", "true");
		params.put("default-Accept", "application/rdf+xml");
		params.put("default-Content-Type", "application/rdf+xml");
		params.put("/summary", "org.eclipse.rdf4j.workbench.commands.SummaryServlet");
		params.put("/info", "org.eclipse.rdf4j.workbench.commands.InfoServlet");
		params.put("/information", "org.eclipse.rdf4j.workbench.commands.InformationServlet");
		params.put("/repositories", "org.eclipse.rdf4j.workbench.commands.RepositoriesServlet");
		params.put("/create", "org.eclipse.rdf4j.workbench.commands.CreateServlet");
		params.put("/delete", "org.eclipse.rdf4j.workbench.commands.DeleteServlet");
		params.put("/namespaces", "org.eclipse.rdf4j.workbench.commands.NamespacesServlet");
		params.put("/contexts", "org.eclipse.rdf4j.workbench.commands.ContextsServlet");
		params.put("/types", "org.eclipse.rdf4j.workbench.commands.TypesServlet");
		params.put("/explore", "org.eclipse.rdf4j.workbench.commands.ExploreServlet");
		params.put("/query", "org.eclipse.rdf4j.workbench.commands.QueryServlet");
		params.put("/saved-queries", "org.eclipse.rdf4j.workbench.commands.SavedQueriesServlet");
		params.put("/export", "org.eclipse.rdf4j.workbench.commands.ExportServlet");
		params.put("/add", "org.eclipse.rdf4j.workbench.commands.AddServlet");
		params.put("/remove", "org.eclipse.rdf4j.workbench.commands.RemoveServlet");
		params.put("/clear", "org.eclipse.rdf4j.workbench.commands.ClearServlet");
		params.put("/update", "org.eclipse.rdf4j.workbench.commands.UpdateServlet");
		return params;
	}

	@Bean
	FilterRegistrationBean<CookieCacheControlFilter> cookieCacheFilter() {
		FilterRegistrationBean<CookieCacheControlFilter> registration = new FilterRegistrationBean<>(
				new CookieCacheControlFilter());
		registration.addUrlPatterns("/rdf4j-workbench/repositories/*");
		registration.setName("cache");
		registration.setOrder(1);
		return registration;
	}

	@Bean
	FilterRegistrationBean<CacheFilter> cacheFilter() {
		FilterRegistrationBean<CacheFilter> registration = new FilterRegistrationBean<>(new CacheFilter());
		registration.addUrlPatterns("/rdf4j-workbench/*");
		registration.setName("CacheFilter");
		registration.setOrder(2);
		registration.addInitParameter("Cache-Control", "600");
		return registration;
	}

	static class ServerXmlWebApplicationContext extends XmlWebApplicationContext {
		ServerXmlWebApplicationContext() {
			setAllowBeanDefinitionOverriding(true);
			setClassLoader(Rdf4jServerWorkbenchApplication.class.getClassLoader());
		}
	}
}