diff --git a/tools/pom.xml b/tools/pom.xml index 6842c44b1f9..faf23eb0c5a 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -19,6 +19,7 @@ workbench runtime runtime-osgi + server-jar diff --git a/tools/server-jar/pom.xml b/tools/server-jar/pom.xml new file mode 100644 index 00000000000..45fb65aa6ca --- /dev/null +++ b/tools/server-jar/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + org.eclipse.rdf4j + rdf4j-tools + 5.1.3-SNAPSHOT + + rdf4j-http-server-jar + RDF4J: HTTP server - self-contained executable + A self-contained executable of the RDF4J Server + + 2.7.18 + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + ${project.groupId} + rdf4j-http-server-spring + ${project.version} + + + ${project.groupId} + rdf4j-config + ${project.version} + + + org.eclipse.rdf4j + rdf4j-tools-federation + ${project.version} + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework + spring-jcl + + + ch.qos.logback + logback-classic + + + org.apache.tomcat.embed + tomcat-embed-core + + + org.apache.tomcat.embed + tomcat-embed-websocket + + + + + org.springframework.boot + spring-boot-starter-jetty + + + org.springframework.boot + spring-boot-starter-validation + + + ch.qos.logback + logback-classic + + + org.slf4j + slf4j-api + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + org.eclipse.rdf4j.server.Application + + + + + repackage + + + + + + + diff --git a/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/Application.java b/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/Application.java new file mode 100644 index 00000000000..e9583fb5671 --- /dev/null +++ b/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/Application.java @@ -0,0 +1,17 @@ +package org.eclipse.rdf4j.server; + +import org.eclipse.rdf4j.common.annotation.Experimental; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; + +@Experimental +@SpringBootApplication +@PropertySource( + value = "classpath:org/eclipse/rdf4j/http/server/application.properties", ignoreResourceNotFound = true, name = "org.springframework.context.support.PropertySourcesPlaceholderConfigurer" +) +public class Application { + public static void main(final String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/MvcConfiguration.java b/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/MvcConfiguration.java new file mode 100644 index 00000000000..72c5ebc5213 --- /dev/null +++ b/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/MvcConfiguration.java @@ -0,0 +1,17 @@ +package org.eclipse.rdf4j.server; + +import java.util.List; + +import org.eclipse.rdf4j.http.server.ProtocolExceptionResolver; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class MvcConfiguration implements WebMvcConfigurer { + @Override + public void configureHandlerExceptionResolvers(@NonNull final List resolvers) { + resolvers.add(new ProtocolExceptionResolver()); + } +} diff --git a/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/ProtocolControllers.java b/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/ProtocolControllers.java new file mode 100644 index 00000000000..03ddff91c9e --- /dev/null +++ b/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/ProtocolControllers.java @@ -0,0 +1,15 @@ +package org.eclipse.rdf4j.server; + +import org.eclipse.rdf4j.http.server.protocol.ProtocolController; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; + +@Configuration +public class ProtocolControllers { + @NonNull + @Bean(name = "rdf4jProtocolController") + public ProtocolController rdf4jProtocolController() { + return new ProtocolController(); + } +} diff --git a/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/RepositoryControllers.java b/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/RepositoryControllers.java new file mode 100644 index 00000000000..b521288a5a6 --- /dev/null +++ b/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/RepositoryControllers.java @@ -0,0 +1,126 @@ +package org.eclipse.rdf4j.server; + +import org.eclipse.rdf4j.http.server.repository.RepositoryController; +import org.eclipse.rdf4j.http.server.repository.RepositoryInterceptor; +import org.eclipse.rdf4j.http.server.repository.RepositoryListController; +import org.eclipse.rdf4j.http.server.repository.config.ConfigController; +import org.eclipse.rdf4j.http.server.repository.contexts.ContextsController; +import org.eclipse.rdf4j.http.server.repository.graph.GraphController; +import org.eclipse.rdf4j.http.server.repository.namespaces.NamespaceController; +import org.eclipse.rdf4j.http.server.repository.namespaces.NamespacesController; +import org.eclipse.rdf4j.http.server.repository.size.SizeController; +import org.eclipse.rdf4j.http.server.repository.statements.StatementsController; +import org.eclipse.rdf4j.http.server.repository.transaction.TransactionController; +import org.eclipse.rdf4j.http.server.repository.transaction.TransactionStartController; +import org.eclipse.rdf4j.repository.manager.RepositoryManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.web.context.annotation.RequestScope; + +@Configuration +public class RepositoryControllers { + + @NonNull + @Autowired + @Bean(name = "rdf4jRepositoryInterceptor") + @RequestScope + public RepositoryInterceptor rdf4jRepositoryInterceptor( + @NonNull @Qualifier("rdf4jRepositoryManager") final RepositoryManager repositoryManager) { + final RepositoryInterceptor interceptor = new RepositoryInterceptor(); + interceptor.setRepositoryManager(repositoryManager); + + return interceptor; + } + + @NonNull + @Autowired + @Bean(name = "rdf4jRepositoryController") + public RepositoryController repositoryController( + @NonNull @Qualifier("rdf4jRepositoryManager") final RepositoryManager repositoryManager) { + final RepositoryController controller = new RepositoryController(); + controller.setRepositoryManager(repositoryManager); + + return controller; + } + + @NonNull + @Autowired + @Bean(name = "rdf4jRepositoryListController") + public RepositoryListController repositoryListController( + @NonNull @Qualifier("rdf4jRepositoryManager") final RepositoryManager repositoryManager) { + final RepositoryListController controller = new RepositoryListController(); + controller.setRepositoryManager(repositoryManager); + + return controller; + } + + @NonNull + @Autowired + @Bean(name = "rdf4jRepositoryConfigController") + public ConfigController rdf4jRepositoryConfigController( + @NonNull @Qualifier("rdf4jRepositoryManager") final RepositoryManager repositoryManager) { + final ConfigController controller = new ConfigController(); + controller.setRepositoryManager(repositoryManager); + + return controller; + } + + @NonNull + @Bean(name = "rdf4jRepositoryContextsController") + public ContextsController rdf4jRepositoryContextsController() { + return new ContextsController(); + } + + @NonNull + @Bean(name = "rdf4jRepositoryNamespacesController") + public NamespacesController rdf4jRepositoryNamespacesController() { + return new NamespacesController(); + } + + @NonNull + @Bean(name = "rdf4jRepositoryNamespaceController") + public NamespaceController rdf4jRepositoryNamespaceController() { + return new NamespaceController(); + } + + @NonNull + @Bean(name = "rdf4jRepositorySizeController") + public SizeController rdf4jRepositorySizeController() { + return new SizeController(); + } + + @NonNull + @Bean(name = "rdf4jRepositoryStatementsController") + public StatementsController rdf4jRepositoryStatementsController() { + return new StatementsController(); + } + + @NonNull + @Bean(name = "rdf4jRepositoryGraphController") + public GraphController rdf4jRepositoryGraphController() { + return new GraphController(); + } + + @NonNull + @Bean(name = "rdf4jRepositoryTransactionController") + public TransactionController rdf4jRepositoryTransactionController() { + return new TransactionController(); + } + + @NonNull + @Bean(name = "rdf4jRepositoryTransactionStartController") + public TransactionStartController rdf4jRepositoryTransactionStartController( + @Nullable @Value("${rdf4j.externalurl:#{null}}") final String externalUrl + ) { + final TransactionStartController transactionStartController = new TransactionStartController(); + transactionStartController.setExternalUrl(externalUrl); + + return transactionStartController; + } + +} diff --git a/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/Routes.java b/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/Routes.java new file mode 100644 index 00000000000..2f0fe9b4b4d --- /dev/null +++ b/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/Routes.java @@ -0,0 +1,106 @@ +package org.eclipse.rdf4j.server; + +import static org.eclipse.rdf4j.http.server.repository.RepositoryInterceptor.REPOSITORY_KEY; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.eclipse.rdf4j.http.server.protocol.ProtocolController; +import org.eclipse.rdf4j.http.server.protocol.ProtocolInterceptor; +import org.eclipse.rdf4j.http.server.repository.RepositoryController; +import org.eclipse.rdf4j.http.server.repository.RepositoryInterceptor; +import org.eclipse.rdf4j.http.server.repository.RepositoryListController; +import org.eclipse.rdf4j.http.server.repository.config.ConfigController; +import org.eclipse.rdf4j.http.server.repository.contexts.ContextsController; +import org.eclipse.rdf4j.http.server.repository.graph.GraphController; +import org.eclipse.rdf4j.http.server.repository.namespaces.NamespaceController; +import org.eclipse.rdf4j.http.server.repository.namespaces.NamespacesController; +import org.eclipse.rdf4j.http.server.repository.size.SizeController; +import org.eclipse.rdf4j.http.server.repository.statements.StatementsController; +import org.eclipse.rdf4j.http.server.repository.transaction.TransactionController; +import org.eclipse.rdf4j.http.server.repository.transaction.TransactionStartController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; + +@Configuration +public class Routes { + @NonNull + @Autowired + @Bean(name = "rdf4jProtocolUrlMapping") + public HandlerMapping rdf4jProtocolUrlMapping( + @NonNull @Qualifier("rdf4jProtocolController") final ProtocolController protocolController) { + final Map urlMap = new LinkedHashMap<>(1); + + urlMap.put("/protocol", protocolController); + + final SimpleUrlHandlerMapping mappings = new SimpleUrlHandlerMapping(); + mappings.setUrlMap(urlMap); + mappings.setOrder(0); + mappings.setAlwaysUseFullPath(true); + mappings.setInterceptors(new ProtocolInterceptor()); + + return mappings; + } + + @NonNull + @Autowired + @Bean(name = "rdf4jRepositoryListUrlMapping") + public HandlerMapping rdf4jRepositoryListUrlMapping( + @NonNull @Qualifier("rdf4jRepositoryListController") final RepositoryListController repositoryListController) { + final Map urlMap = new LinkedHashMap<>(1); + + urlMap.put("/repositories", repositoryListController); + + final SimpleUrlHandlerMapping mappings = new SimpleUrlHandlerMapping(); + mappings.setUrlMap(urlMap); + mappings.setOrder(1); + mappings.setAlwaysUseFullPath(true); + + return mappings; + } + + @NonNull + @Autowired + @Bean(name = "rdf4jRepositoryUrlMapping") + public HandlerMapping rdf4jRepositoryUrlMapping( + @NonNull @Qualifier("rdf4jRepositoryController") final RepositoryController repositoryController, + @NonNull @Qualifier("rdf4jRepositoryConfigController") final ConfigController repositoryConfigController, + @NonNull @Qualifier("rdf4jRepositoryContextsController") final ContextsController contextsController, + @NonNull @Qualifier("rdf4jRepositoryNamespacesController") final NamespacesController namespacesController, + @NonNull @Qualifier("rdf4jRepositoryNamespaceController") final NamespaceController namespaceController, + @NonNull @Qualifier("rdf4jRepositorySizeController") final SizeController sizeController, + @NonNull @Qualifier("rdf4jRepositoryStatementsController") final StatementsController statementsController, + @NonNull @Qualifier("rdf4jRepositoryGraphController") final GraphController graphController, + @NonNull @Qualifier("rdf4jRepositoryTransactionController") final TransactionController transactionController, + @NonNull @Qualifier("rdf4jRepositoryTransactionStartController") final TransactionStartController transactionStartController, + @NonNull @Qualifier("rdf4jRepositoryInterceptor") final RepositoryInterceptor repositoryInterceptor + ) { + final Map urlMap = new LinkedHashMap<>(11); + + final String repositoryPrefix = "/repositories/{" + REPOSITORY_KEY + "}"; + urlMap.put(repositoryPrefix + "/namespaces/{nsPrefix}", namespaceController); + urlMap.put(repositoryPrefix + "/namespaces", namespacesController); + urlMap.put(repositoryPrefix + "/config", repositoryConfigController); + urlMap.put(repositoryPrefix + "/contexts", contextsController); + urlMap.put(repositoryPrefix + "/statements", statementsController); + urlMap.put(repositoryPrefix + "/rdf-graphs", contextsController); + urlMap.put(repositoryPrefix + "/rdf-graphs/{graph}", graphController); + urlMap.put(repositoryPrefix + "/size", sizeController); + urlMap.put(repositoryPrefix + "/transactions", transactionStartController); + urlMap.put(repositoryPrefix + "/transactions/{xid}", transactionController); + urlMap.put(repositoryPrefix, repositoryController); + + final SimpleUrlHandlerMapping mappings = new SimpleUrlHandlerMapping(); + mappings.setUrlMap(urlMap); + mappings.setOrder(2); + mappings.setAlwaysUseFullPath(true); + mappings.setInterceptors(repositoryInterceptor); + + return mappings; + } +} diff --git a/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/ServiceConfiguration.java b/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/ServiceConfiguration.java new file mode 100644 index 00000000000..df19f8028ec --- /dev/null +++ b/tools/server-jar/src/main/java/org/eclipse/rdf4j/server/ServiceConfiguration.java @@ -0,0 +1,54 @@ +package org.eclipse.rdf4j.server; + +import java.util.Objects; + +import org.eclipse.rdf4j.common.app.AppConfiguration; +import org.eclipse.rdf4j.repository.manager.LocalRepositoryManager; +import org.eclipse.rdf4j.repository.manager.RepositoryManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.lang.NonNull; + +@Configuration +public class ServiceConfiguration { + @NonNull + @Bean(name = "commonAppConfig", initMethod = "init", destroyMethod = "destroy") + public AppConfiguration commonAppConfig() { + final AppConfiguration config = new AppConfiguration(); + config.setApplicationId("Server"); + config.setLongName("RDF4J Server"); + + return config; + } + + @NonNull + @Autowired + @Bean(name = "rdf4jRepositoryManager", initMethod = "init", destroyMethod = "shutDown") + @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) + public RepositoryManager rdf4jRepositoryManager( + @NonNull @Qualifier("commonAppConfig") final AppConfiguration appConfig) { + Objects.requireNonNull(appConfig, "Application config was not properly initialized!"); + + return new LocalRepositoryManager(appConfig.getDataDir()); + } + + @NonNull + @Bean(name = "messageSource") + public MessageSource messageSource() { + final ResourceBundleMessageSource bundle = new ResourceBundleMessageSource(); + + bundle.setBasenames( + "org.eclipse.rdf4j.http.server.messages", + "org.eclipse.rdf4j.common.webapp.system.messages", + "org.eclipse.rdf4j.common.webapp.messages" + ); + + return bundle; + } +} diff --git a/tools/server-jar/src/main/resources/logback.xml b/tools/server-jar/src/main/resources/logback.xml new file mode 100644 index 00000000000..962f9889af5 --- /dev/null +++ b/tools/server-jar/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/RepositoryInterceptor.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/RepositoryInterceptor.java index 98d50d996dc..bed6c0aebf2 100644 --- a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/RepositoryInterceptor.java +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/RepositoryInterceptor.java @@ -12,6 +12,7 @@ import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import java.util.Map; import java.util.Objects; import javax.servlet.http.HttpServletRequest; @@ -30,6 +31,7 @@ import org.eclipse.rdf4j.rio.helpers.BasicParserSettings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.web.servlet.HandlerMapping; /** * Interceptor for repository requests. Should not be a singleton bean! Configure as inner bean in openrdf-servlet.xml @@ -47,7 +49,7 @@ public class RepositoryInterceptor extends ServerInterceptor { private static final String REPOSITORY_ID_KEY = "repositoryID"; - private static final String REPOSITORY_KEY = "repository"; + public static final String REPOSITORY_KEY = "repository"; /*-----------* * Variables * @@ -80,6 +82,17 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons } } + if (repositoryID == null) { + final Object pathVariables = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + + // noinspection unchecked + repositoryID = ((Map) pathVariables).get(REPOSITORY_KEY); + } + + if (repositoryID != null) { + logger.debug("repositoryID is '{}'", repositoryID); + } + ProtocolUtil.logRequestParameters(request); return super.preHandle(request, respons, handler); diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/namespaces/NamespaceController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/namespaces/NamespaceController.java index 070dfd5b55d..85cce6268f7 100644 --- a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/namespaces/NamespaceController.java +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/namespaces/NamespaceController.java @@ -35,6 +35,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContextException; +import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; @@ -55,7 +56,15 @@ public NamespaceController() throws ApplicationContextException { protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { String pathInfoStr = request.getPathInfo(); - String prefix = pathInfoStr.substring(pathInfoStr.lastIndexOf('/') + 1); + final String prefix; + if (pathInfoStr != null) { + prefix = pathInfoStr.substring(pathInfoStr.lastIndexOf('/') + 1); + } else { + final Object pathVariables = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + + // noinspection unchecked + prefix = ((Map) pathVariables).get("nsPrefix"); + } String reqMethod = request.getMethod(); diff --git a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/TransactionController.java b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/TransactionController.java index f0a2b46612b..83c88b38b8a 100644 --- a/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/TransactionController.java +++ b/tools/server-spring/src/main/java/org/eclipse/rdf4j/http/server/repository/transaction/TransactionController.java @@ -91,6 +91,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ApplicationContextException; +import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.View; import org.springframework.web.servlet.mvc.AbstractController; @@ -215,7 +216,6 @@ private UUID getTransactionID(HttpServletRequest request) throws ClientHTTPExcep if (pathInfo.length == 3) { try { txnID = UUID.fromString(pathInfo[2]); - logger.debug("txnID is '{}'", txnID); } catch (IllegalArgumentException e) { throw new ClientHTTPException(SC_BAD_REQUEST, "not a valid transaction id: " + pathInfo[2]); } @@ -224,6 +224,20 @@ private UUID getTransactionID(HttpServletRequest request) throws ClientHTTPExcep } } + if (txnID == null) { + final Object pathVariables = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + + // noinspection unchecked + final String xidStr = ((Map) pathVariables).get("xid"); + try { + txnID = UUID.fromString(xidStr); + } catch (IllegalArgumentException e) { + throw new ClientHTTPException(SC_BAD_REQUEST, "not a valid transaction id: " + xidStr); + } + } + + logger.debug("txnID is '{}'", txnID); + return txnID; }