spark.Service Maven / Gradle / Ivy
Show all versions of spark-core Show documentation
/*
* Copyright 2015 - Per Wendel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package spark;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import spark.embeddedserver.EmbeddedServer;
import spark.embeddedserver.EmbeddedServers;
import spark.embeddedserver.jetty.EmbeddedJettyServer;
import spark.embeddedserver.jetty.websocket.WebSocketHandlerClassWrapper;
import spark.embeddedserver.jetty.websocket.WebSocketHandlerInstanceWrapper;
import spark.embeddedserver.jetty.websocket.WebSocketHandlerWrapper;
import spark.route.HttpMethod;
import spark.route.Routes;
import spark.route.ServletRoutes;
import spark.routematch.RouteMatch;
import spark.ssl.SslStores;
import spark.staticfiles.MimeType;
import spark.staticfiles.StaticFilesConfiguration;
import static java.util.Objects.requireNonNull;
import static spark.globalstate.ServletFlag.isRunningFromServlet;
/**
* Represents a Spark server "session".
* If a user wants multiple 'Sparks' in his application the method {@link Service#ignite()} should be statically
* imported and used to create instances. The instance should typically be named so when prefixing the 'routing' methods
* the semantic makes sense. For example 'http' is a good variable name since when adding routes it would be:
* Service http = ignite();
* ...
* http.get("/hello", (q, a) {@literal ->} "Hello World");
*/
public final class Service extends Routable {
private static final Logger LOG = LoggerFactory.getLogger("spark.Spark");
public static final int SPARK_DEFAULT_PORT = 4567;
protected static final String DEFAULT_ACCEPT_TYPE = "*/*";
protected boolean initialized = false;
protected int port = SPARK_DEFAULT_PORT;
protected String ipAddress = "0.0.0.0";
protected SslStores sslStores;
protected Map webSocketHandlers = null;
protected int maxThreads = -1;
protected int minThreads = -1;
protected int threadIdleTimeoutMillis = -1;
protected Optional webSocketIdleTimeoutMillis = Optional.empty();
private boolean useVirtualThreads;
protected EmbeddedServer server;
protected Deque pathDeque = new ArrayDeque<>();
protected Routes routes;
private CountDownLatch initLatch = new CountDownLatch(1);
private CountDownLatch stopLatch = new CountDownLatch(0);
private Object embeddedServerIdentifier = EmbeddedServers.defaultIdentifier();
public final Redirect redirect;
public final StaticFiles staticFiles;
private final StaticFilesConfiguration staticFilesConfiguration;
private final ExceptionMapper exceptionMapper = new ExceptionMapper();
// default exception handler during initialization phase
private Consumer initExceptionHandler = (e) -> {
LOG.error("ignite failed", e);
System.exit(100);
};
private boolean trustForwardHeaders = true;
/**
* Creates a new Service (a Spark instance). This should be used instead of the static API if the user wants
* multiple services in one process.
*
* @return the newly created object
*/
public static Service ignite() {
return new Service();
}
private Service() {
redirect = Redirect.create(this);
staticFiles = new StaticFiles();
if (isRunningFromServlet()) {
staticFilesConfiguration = StaticFilesConfiguration.servletInstance;
} else {
staticFilesConfiguration = StaticFilesConfiguration.create();
}
}
/**
* Set the identifier used to select the EmbeddedServer;
* null for the default.
*
* @param obj the identifier passed to {@link EmbeddedServers}.
*/
public synchronized void embeddedServerIdentifier(Object obj) {
if (initialized) {
throwBeforeRouteMappingException();
}
embeddedServerIdentifier = obj;
}
/**
* Get the identifier used to select the EmbeddedServer;
* null for the default.
*/
public synchronized Object embeddedServerIdentifier() {
return embeddedServerIdentifier;
}
/**
* Set the IP address that Spark should listen on. If not called the default
* address is '0.0.0.0'. This has to be called before any route mapping is
* done.
*
* @param ipAddress The ipAddress
* @return the object with IP address set
*/
public synchronized Service ipAddress(String ipAddress) {
if (initialized) {
throwBeforeRouteMappingException();
}
this.ipAddress = ipAddress;
return this;
}
/**
* Set the port that Spark should listen on. If not called the default port
* is 4567. This has to be called before any route mapping is done.
* If provided port = 0 then the an arbitrary available port will be used.
*
* @param port The port number
* @return the object with port set
*/
public synchronized Service port(int port) {
if (initialized) {
throwBeforeRouteMappingException();
}
this.port = port;
return this;
}
/**
* Retrieves the port that Spark is listening on.
*
* @return The port Spark server is listening on.
* @throws IllegalStateException when the server is not started
*/
public synchronized int port() {
if (initialized) {
return port;
} else {
throw new IllegalStateException("This must be done after route mapping has begun");
}
}
/**
* Set the connection to be secure, using the specified keystore and
* truststore. This has to be called before any route mapping is done. You
* have to supply a keystore file, truststore file is optional (keystore
* will be reused). By default, client certificates are not checked.
* This method is only relevant when using embedded Jetty servers. It should
* not be used if you are using Servlets, where you will need to secure the
* connection in the servlet container
*
* @param keystoreFile The keystore file location as string
* @param keystorePassword the password for the keystore
* @param truststoreFile the truststore file location as string, leave null to reuse
* keystore
* @param truststorePassword the trust store password
* @return the object with connection set to be secure
*/
public synchronized Service secure(String keystoreFile,
String keystorePassword,
String truststoreFile,
String truststorePassword) {
return secure(keystoreFile, keystorePassword, null, truststoreFile, truststorePassword, false);
}
/**
* Set the connection to be secure, using the specified keystore and
* truststore. This has to be called before any route mapping is done. You
* have to supply a keystore file, truststore file is optional (keystore
* will be reused). By default, client certificates are not checked.
* This method is only relevant when using embedded Jetty servers. It should
* not be used if you are using Servlets, where you will need to secure the
* connection in the servlet container
*
* @param keystoreFile The keystore file location as string
* @param keystorePassword the password for the keystore
* @param certAlias the default certificate Alias
* @param truststoreFile the truststore file location as string, leave null to reuse
* keystore
* @param truststorePassword the trust store password
* @return the object with connection set to be secure
*/
public synchronized Service secure(String keystoreFile,
String keystorePassword,
String certAlias,
String truststoreFile,
String truststorePassword) {
return secure(keystoreFile, keystorePassword, certAlias, truststoreFile, truststorePassword, false);
}
/**
* Set the connection to be secure, using the specified keystore and
* truststore. This has to be called before any route mapping is done. You
* have to supply a keystore file, truststore file is optional (keystore
* will be reused).
* This method is only relevant when using embedded Jetty servers. It should
* not be used if you are using Servlets, where you will need to secure the
* connection in the servlet container
*
* @param keystoreFile The keystore file location as string
* @param keystorePassword the password for the keystore
* @param truststoreFile the truststore file location as string, leave null to reuse
* keystore
* @param needsClientCert Whether to require client certificate to be supplied in
* request
* @param truststorePassword the trust store password
* @return the object with connection set to be secure
*/
public synchronized Service secure(String keystoreFile,
String keystorePassword,
String truststoreFile,
String truststorePassword,
boolean needsClientCert) {
return secure(keystoreFile, keystorePassword, null, truststoreFile, truststorePassword, needsClientCert);
}
/**
* Set the connection to be secure, using the specified keystore and
* truststore. This has to be called before any route mapping is done. You
* have to supply a keystore file, truststore file is optional (keystore
* will be reused).
* This method is only relevant when using embedded Jetty servers. It should
* not be used if you are using Servlets, where you will need to secure the
* connection in the servlet container
*
* @param keystoreFile The keystore file location as string
* @param keystorePassword the password for the keystore
* @param certAlias the default certificate Alias
* @param truststoreFile the truststore file location as string, leave null to reuse
* keystore
* @param needsClientCert Whether to require client certificate to be supplied in
* request
* @param truststorePassword the trust store password
* @return the object with connection set to be secure
*/
public synchronized Service secure(String keystoreFile,
String keystorePassword,
String certAlias,
String truststoreFile,
String truststorePassword,
boolean needsClientCert) {
if (initialized) {
throwBeforeRouteMappingException();
}
if (keystoreFile == null) {
throw new IllegalArgumentException(
"Must provide a keystore file to run secured");
}
sslStores = SslStores.create(keystoreFile, keystorePassword, certAlias, truststoreFile, truststorePassword, needsClientCert);
return this;
}
/**
* Configures the embedded web server's thread pool.
*
* @param maxThreads max nbr of threads.
* @return the object with the embedded web server's thread pool configured
*/
public synchronized Service threadPool(int maxThreads) {
return threadPool(maxThreads, -1, -1);
}
/**
* Configures the embedded web server's thread pool.
*
* @param maxThreads max nbr of threads.
* @param minThreads min nbr of threads.
* @param idleTimeoutMillis thread idle timeout (ms).
* @return the object with the embedded web server's thread pool configured
*/
public synchronized Service threadPool(int maxThreads, int minThreads, int idleTimeoutMillis) {
if (initialized) {
throwBeforeRouteMappingException();
}
this.maxThreads = maxThreads;
this.minThreads = minThreads;
this.threadIdleTimeoutMillis = idleTimeoutMillis;
return this;
}
/**
* Sets the folder in classpath serving static files. Observe: this method
* must be called before all other methods.
*
* @param folder the folder in classpath.
* @return the object with folder set
*/
public synchronized Service staticFileLocation(String folder) {
if (initialized && !isRunningFromServlet()) {
throwBeforeRouteMappingException();
}
if (!staticFilesConfiguration.isStaticResourcesSet()) {
staticFilesConfiguration.configure(folder);
} else {
LOG.warn("Static file location has already been set");
}
return this;
}
/**
* Sets the external folder serving static files. Observe: this method
* must be called before all other methods.
*
* @param externalFolder the external folder serving static files.
* @return the object with external folder set
*/
public synchronized Service externalStaticFileLocation(String externalFolder) {
if (initialized && !isRunningFromServlet()) {
throwBeforeRouteMappingException();
}
if (!staticFilesConfiguration.isExternalStaticResourcesSet()) {
staticFilesConfiguration.configureExternal(externalFolder);
} else {
LOG.warn("External static file location has already been set");
}
return this;
}
/**
* Unmaps a particular route from the collection of those that have been previously routed.
* Search for previously established routes using the given path and unmaps any matches that are found.
*
* @param path the route path
* @return true if this is a matching route which has been previously routed
* @throws IllegalArgumentException if path is null or blank
*/
public boolean unmap(String path) {
return routes.remove(path);
}
/**
* Unmaps a particular route from the collection of those that have been previously routed.
* Search for previously established routes using the given path and HTTP method, unmaps any
* matches that are found.
*
* @param path the route path
* @param httpMethod the http method
* @return true if this is a matching route that has been previously routed
* @throws IllegalArgumentException if path is null or blank or if httpMethod is null, blank,
* or an invalid HTTP method
*/
public boolean unmap(String path, String httpMethod) {
return routes.remove(path, httpMethod);
}
/**
* Maps the given path to the given WebSocket handler class.
*
* This is currently only available in the embedded server mode.
*
* @param path the WebSocket path.
* @param handlerClass the handler class that will manage the WebSocket connection to the given path.
*/
public void webSocket(String path, Class> handlerClass) {
addWebSocketHandler(path, new WebSocketHandlerClassWrapper(handlerClass));
}
/**
* Maps the given path to the given WebSocket handler instance.
*
* This is currently only available in the embedded server mode.
*
* @param path the WebSocket path.
* @param handler the handler instance that will manage the WebSocket connection to the given path.
*/
public void webSocket(String path, Object handler) {
addWebSocketHandler(path, new WebSocketHandlerInstanceWrapper(handler));
}
private synchronized void addWebSocketHandler(String path, WebSocketHandlerWrapper handlerWrapper) {
if (initialized) {
throwBeforeRouteMappingException();
}
if (isRunningFromServlet()) {
throw new IllegalStateException("WebSockets are only supported in the embedded server");
}
requireNonNull(path, "WebSocket path cannot be null");
if (webSocketHandlers == null) {
webSocketHandlers = new HashMap<>();
}
webSocketHandlers.put(path, handlerWrapper);
}
/**
* Sets the max idle timeout in milliseconds for WebSocket connections.
*
* @param timeoutMillis The max idle timeout in milliseconds.
* @return the object with max idle timeout set for WebSocket connections
*/
public synchronized Service webSocketIdleTimeoutMillis(long timeoutMillis) {
if (initialized) {
throwBeforeRouteMappingException();
}
if (isRunningFromServlet()) {
throw new IllegalStateException("WebSockets are only supported in the embedded server");
}
webSocketIdleTimeoutMillis = Optional.of(timeoutMillis);
return this;
}
/**
* Maps 404 errors to the provided custom page
*
* @param page the custom 404 error page.
*/
public synchronized void notFound(String page) {
CustomErrorPages.add(404, page);
}
/**
* Maps 500 internal server errors to the provided custom page
*
* @param page the custom 500 internal server error page.
*/
public synchronized void internalServerError(String page) {
CustomErrorPages.add(500, page);
}
/**
* Maps 404 errors to the provided route.
*/
public synchronized void notFound(Route route) {
CustomErrorPages.add(404, route);
}
/**
* Maps 500 internal server errors to the provided route.
*/
public synchronized void internalServerError(Route route) {
CustomErrorPages.add(500, route);
}
/**
* Waits for the spark server to be initialized.
* If it's already initialized will return immediately
*/
public void awaitInitialization() {
if (!initialized) {
throw new IllegalStateException("Server has not been properly initialized");
}
try {
initLatch.await();
} catch (InterruptedException e) {
LOG.info("Interrupted by another thread");
Thread.currentThread().interrupt();
}
}
private void throwBeforeRouteMappingException() {
throw new IllegalStateException(
"This must be done before route mapping has begun");
}
private boolean hasMultipleHandlers() {
return webSocketHandlers != null;
}
/**
* Stops the Spark server and clears all routes.
*/
public synchronized void stop() {
if (!initialized) {
return;
}
initiateStop();
}
/**
* Waits for the Spark server to stop.
* Warning: this method should not be called from a request handler.
*/
public void awaitStop() {
try {
stopLatch.await();
} catch (InterruptedException e) {
LOG.warn("Interrupted by another thread");
Thread.currentThread().interrupt();
}
}
private void initiateStop() {
stopLatch = new CountDownLatch(1);
Thread stopThread = new Thread(() -> {
if (server != null) {
server.extinguish();
initLatch = new CountDownLatch(1);
}
routes.clear();
exceptionMapper.clear();
staticFilesConfiguration.clear();
initialized = false;
stopLatch.countDown();
});
stopThread.start();
}
/**
* Add a path-prefix to the routes declared in the routeGroup
* The path() method adds a path-fragment to a path-stack, adds
* routes from the routeGroup, then pops the path-fragment again.
* It's used for separating routes into groups, for example:
* path("/api/email", () -> {
* ....post("/add", EmailApi::addEmail);
* ....put("/change", EmailApi::changeEmail);
* ....etc
* });
* Multiple path() calls can be nested.
*
* @param path the path to prefix routes with
* @param routeGroup group of routes (can also contain path() calls)
*/
public void path(String path, RouteGroup routeGroup) {
pathDeque.addLast(path);
routeGroup.addRoutes();
pathDeque.removeLast();
}
public String getPaths() {
return pathDeque.stream().collect(Collectors.joining(""));
}
/**
* @return all routes information from this service
*/
public List routes() {
return routes.findAll();
}
@Override
public void addRoute(HttpMethod httpMethod, RouteImpl route) {
init();
routes.add(httpMethod, route.withPrefix(getPaths()));
}
@Override
public void addFilter(HttpMethod httpMethod, FilterImpl filter) {
init();
routes.add(httpMethod, filter.withPrefix(getPaths()));
}
@Override
@Deprecated
public void addRoute(String httpMethod, RouteImpl route) {
init();
routes.add(httpMethod + " '" + getPaths() + route.getPath() + "'", route.getAcceptType(), route);
}
@Override
@Deprecated
public void addFilter(String httpMethod, FilterImpl filter) {
init();
routes.add(httpMethod + " '" + getPaths() + filter.getPath() + "'", filter.getAcceptType(), filter);
}
public synchronized void init() {
if (!initialized) {
initializeRouteMatcher();
if (!isRunningFromServlet()) {
new Thread(() -> {
try {
EmbeddedServers.initialize();
if (embeddedServerIdentifier == null) {
embeddedServerIdentifier = EmbeddedServers.defaultIdentifier();
}
server = EmbeddedServers.create(embeddedServerIdentifier,
routes,
exceptionMapper,
staticFilesConfiguration,
hasMultipleHandlers());
server.configureWebSockets(webSocketHandlers, webSocketIdleTimeoutMillis);
server.trustForwardHeaders(trustForwardHeaders);
if(useVirtualThreads && server instanceof EmbeddedJettyServer ejs){
QueuedThreadPool pool=new QueuedThreadPool();
pool.setVirtualThreadsExecutor(Executors.newThreadPerTaskExecutor(new ThreadFactory(){
private int counter=0;
@Override
public Thread newThread(Runnable r){
return Thread.ofVirtual().name("VirtualThread-"+(counter++)).unstarted(r);
}
}));
ejs.withThreadPool(pool);
}
port = server.ignite(
ipAddress,
port,
sslStores,
maxThreads,
minThreads,
threadIdleTimeoutMillis);
} catch (Exception e) {
initExceptionHandler.accept(e);
}
try {
initLatch.countDown();
server.join();
} catch (InterruptedException e) {
LOG.error("server interrupted", e);
Thread.currentThread().interrupt();
}
}).start();
}
initialized = true;
}
}
private void initializeRouteMatcher() {
if (isRunningFromServlet()) {
routes = ServletRoutes.get();
} else {
routes = Routes.create();
}
}
/**
* @return The approximate number of currently active threads in the embedded Jetty server
*/
public synchronized int activeThreadCount() {
if (server != null) {
return server.activeThreadCount();
}
return 0;
}
//////////////////////////////////////////////////
// EXCEPTION mapper
//////////////////////////////////////////////////
/**
* Maps an exception handler to be executed when an exception occurs during routing
*
* @param exceptionClass the exception class
* @param handler The handler
*/
public synchronized void exception(Class exceptionClass, ExceptionHandler super T> handler) {
// wrap
ExceptionHandlerImpl wrapper = new ExceptionHandlerImpl(exceptionClass) {
@Override
public void handle(T exception, Request request, Response response) {
handler.handle(exception, request, response);
}
};
exceptionMapper.map(exceptionClass, wrapper);
}
//////////////////////////////////////////////////
// HALT methods
//////////////////////////////////////////////////
/**
* Immediately stops a request within a filter or route
* NOTE: When using this don't catch exceptions of type HaltException, or if catched, re-throw otherwise
* halt will not work
*
* @return HaltException object
*/
public HaltException halt() {
throw new HaltException();
}
/**
* Immediately stops a request within a filter or route with specified status code
* NOTE: When using this don't catch exceptions of type HaltException, or if catched, re-throw otherwise
* halt will not work
*
* @param status the status code
* @return HaltException object with status code set
*/
public HaltException halt(int status) {
throw new HaltException(status);
}
/**
* Immediately stops a request within a filter or route with specified body content
* NOTE: When using this don't catch exceptions of type HaltException, or if catched, re-throw otherwise
* halt will not work
*
* @param body The body content
* @return HaltException object with body set
*/
public HaltException halt(String body) {
throw new HaltException(body);
}
/**
* Immediately stops a request within a filter or route with specified status code and body content
* NOTE: When using this don't catch exceptions of type HaltException, or if catched, re-throw otherwise
* halt will not work
*
* @param status The status code
* @param body The body content
* @return HaltException object with status and body set
*/
public HaltException halt(int status, String body) {
throw new HaltException(status, body);
}
/**
* Sets Spark to trust the HTTP headers that are commonly used in reverse proxies.
* More info at https://www.eclipse.org/jetty/javadoc/current/org/eclipse/jetty/server/ForwardedRequestCustomizer.html
*/
public synchronized Service trustForwardHeaders() {
if (initialized) {
throwBeforeRouteMappingException();
}
this.trustForwardHeaders = true;
return this;
}
/**
* Sets Spark to NOT trust the HTTP headers that are commonly used in reverse proxies.
* More info at https://www.eclipse.org/jetty/javadoc/current/org/eclipse/jetty/server/ForwardedRequestCustomizer.html
*/
public synchronized Service untrustForwardHeaders() {
if (initialized) {
throwBeforeRouteMappingException();
}
this.trustForwardHeaders = false;
return this;
}
/**
* Overrides default exception handler during initialization phase
*
* @param initExceptionHandler The custom init exception handler
*/
public void initExceptionHandler(Consumer initExceptionHandler) {
if (initialized) {
throwBeforeRouteMappingException();
}
this.initExceptionHandler = initExceptionHandler;
}
public void setUseVirtualThreads(boolean useVirtualThreads){
this.useVirtualThreads=useVirtualThreads;
}
/**
* Provides static files utility methods.
*/
public final class StaticFiles {
/**
* Sets the folder in classpath serving static files. Observe: this method
* must be called before all other methods.
*
* @param folder the folder in classpath.
*/
public void location(String folder) {
staticFileLocation(folder);
}
/**
* Sets the external folder serving static files. Observe: this method
* must be called before all other methods.
*
* @param externalFolder the external folder serving static files.
*/
public void externalLocation(String externalFolder) {
externalStaticFileLocation(externalFolder);
}
/**
* Puts custom headers for static resources. If the headers previously contained mapping for
* a specific key in the provided headers map, the old value is replaced by the specified value.
*
* @param headers the headers to set on static resources
*/
public void headers(Map headers) {
staticFilesConfiguration.putCustomHeaders(headers);
}
/**
* Puts custom header for static resources. If the headers previously contained a mapping for
* the key, the old value is replaced by the specified value.
*
* @param key the key
* @param value the value
*/
public void header(String key, String value) {
staticFilesConfiguration.putCustomHeader(key, value);
}
/**
* Sets the expire-time for static resources
*
* @param seconds the expire time in seconds
*/
@Experimental("Functionality will not be removed. The API might change")
public void expireTime(long seconds) {
staticFilesConfiguration.setExpireTimeSeconds(seconds);
}
/**
* Maps an extension to a mime-type. This will overwrite any previous mappings.
*
* @param extension the extension to be mapped
* @param mimeType the mime-type for the extension
*/
public void registerMimeType(String extension, String mimeType) {
MimeType.register(extension, mimeType);
}
/**
* Disables the automatic setting of Content-Type header made from a guess based on extension.
*/
public void disableMimeTypeGuessing() {
MimeType.disableGuessing();
}
}
}