All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.helger.photon.jetty.JettyStarter Maven / Gradle / Ivy

/*
 * Copyright (C) 2014-2022 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * 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 com.helger.photon.jetty;

import java.io.File;
import java.util.function.IntUnaryOperator;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;

import org.eclipse.jetty.annotations.AnnotationConfiguration;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.session.DefaultSessionCache;
import org.eclipse.jetty.server.session.FileSessionDataStore;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.FragmentConfiguration;
import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
import org.eclipse.jetty.webapp.MetaInfConfiguration;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.webapp.WebInfConfiguration;
import org.eclipse.jetty.webapp.WebXmlConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.CGlobal;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.OverrideOnDemand;
import com.helger.commons.io.file.FilenameHelper;
import com.helger.commons.lang.ClassHelper;
import com.helger.commons.string.StringHelper;
import com.helger.commons.system.SystemProperties;

/**
 * Run a standalone web application in Jetty on port 8080.
* http://localhost:8080/ * * @author Philip Helger */ @NotThreadSafe public class JettyStarter { public static final String CONTAINER_INCLUDE_JAR_PATTERN_JAR = ".*\\.jar$*"; public static final String CONTAINER_INCLUDE_JAR_PATTERN_CLASSES = ".*/classes/.*"; public static final String CONTAINER_INCLUDE_JAR_PATTERN_ALL = CONTAINER_INCLUDE_JAR_PATTERN_JAR + "|" + CONTAINER_INCLUDE_JAR_PATTERN_CLASSES; public static final int DEFAULT_PORT = 8080; public static final String DEFAULT_STOP_KEY = InternalJettyStopMonitorThread.STOP_KEY; public static final int DEFAULT_STOP_PORT = InternalJettyStopMonitorThread.STOP_PORT; public static final String DEFAULT_CONTEXT_PATH = "/"; public static final String DEFAULT_CONTAINER_INCLUDE_JAR_PATTERN = null; public static final String DEFAULT_WEB_INF_INCLUDE_JAR_PATTERN = null; public static final String DEFAULT_SESSION_COOKIE_NAME = "PHOTONSESSIONID"; public static final boolean DEFAULT_ALLOW_ANNOTATION_BASED_CONFIG = true; public static final boolean DEFAULT_ALLOW_DIRECTORY_LISTING = false; private static final Logger LOGGER = LoggerFactory.getLogger (JettyStarter.class); private final String m_sAppName; private final String m_sDirBaseName; private int m_nPort = DEFAULT_PORT; private boolean m_bRunStopMonitor = true; private String m_sStopKey = DEFAULT_STOP_KEY; private IntUnaryOperator m_aStopPort = x -> DEFAULT_STOP_PORT; private boolean m_bSpecialSessionMgr = true; private Resource m_aResourceBase = _asRes ("target/webapp-classes"); private String m_sWebXmlResource; private String m_sContextPath = DEFAULT_CONTEXT_PATH; private String m_sContainerIncludeJarPattern = DEFAULT_CONTAINER_INCLUDE_JAR_PATTERN; private String m_sWebInfIncludeJarPattern = DEFAULT_WEB_INF_INCLUDE_JAR_PATTERN; private ThreadPool m_aThreadPool; private boolean m_bAllowAnnotationBasedConfig = DEFAULT_ALLOW_ANNOTATION_BASED_CONFIG; private boolean m_bAllowDirectoryListing = DEFAULT_ALLOW_DIRECTORY_LISTING; private String m_sSessionCookieName = DEFAULT_SESSION_COOKIE_NAME; @Nonnull private static Resource _asRes (@Nonnull final String sPath) { try { return Resource.newResource (sPath); } catch (final Exception ex) { throw new IllegalArgumentException ("Invalid resource path '" + sPath + "'"); } } public JettyStarter (@Nonnull final Class aAppClass) { this (ClassHelper.getClassLocalName (aAppClass)); } public JettyStarter (@Nonnull @Nonempty final String sAppName) { ValueEnforcer.notEmpty (sAppName, "AppName"); m_sAppName = sAppName; m_sDirBaseName = FilenameHelper.getAsSecureValidFilename (sAppName); if (StringHelper.hasNoText (m_sDirBaseName)) throw new IllegalStateException ("FolderName for application name '" + sAppName + "' is empty."); // Must be directly called on System to have an effect! System.setProperty ("log4j2.disable.jmx", "true"); } /** * @return Port to run on. */ @Nonnegative public int getPort () { return m_nPort; } /** * Set the port to be used to run the application. Defaults to * {@value #DEFAULT_PORT} * * @param nPort * The port to be used. Must be > 0. * @return this for chaining */ @Nonnull public final JettyStarter setPort (@Nonnegative final int nPort) { ValueEnforcer.isGT0 (nPort, "Port"); m_nPort = nPort; return this; } public boolean isRunStopMonitor () { return m_bRunStopMonitor; } /** * Enable or disable the "stop monitor" that listens for the graceful * shutdown. By default this is enabled. * * @param bRunStopMonitor * true to enable it, false to disable it. * @return this for chaining */ @Nonnull public final JettyStarter setRunStopMonitor (final boolean bRunStopMonitor) { m_bRunStopMonitor = bRunStopMonitor; return this; } @Nonnull public String getStopKey () { return m_sStopKey; } /** * Set the hidden "stop key" that must be submitted to stop the server. * Defaults to {@link #DEFAULT_STOP_KEY}. If set here, it must also be set in * {@link JettyStopper}. * * @param sStopKey * The stop key to be used. May not be null. * @return this for chaining */ @Nonnull public final JettyStarter setStopKey (@Nonnull final String sStopKey) { ValueEnforcer.notNull (sStopKey, "StopKey"); m_sStopKey = sStopKey; return this; } public int getStopPort () { return m_aStopPort.applyAsInt (m_nPort); } /** * Set the port on which the "stop monitor" should be running. Defaults to * {@link #DEFAULT_STOP_PORT}. When running multiple Jettys at once, each * instance must use it's own stop port. If this is set here, it must also be * set in {@link JettyStopper}. * * @param nStopPort * The stop port to be used. Must be > 0. * @return this for chaining */ @Nonnull public final JettyStarter setStopPort (@Nonnegative final int nStopPort) { ValueEnforcer.isGT0 (nStopPort, "StopPort"); // Set a constant value return setStopPort (x -> nStopPort); } /** * Set the port on which the "stop monitor" should be running. Defaults to * {@link #DEFAULT_STOP_PORT}. When running multiple Jettys at once, each * instance must use it's own stop port. If this is set here, it must also be * set in {@link JettyStopper}.
* This overload lets you set a function that takes as input the default port * and you can calculate the stop port from it. * * @param aStopPort * The stop port to be used. May not be null. * @return this for chaining * @since 8.3.2 */ @Nonnull public final JettyStarter setStopPort (@Nonnull final IntUnaryOperator aStopPort) { ValueEnforcer.notNull (aStopPort, "StopPort"); m_aStopPort = aStopPort; return this; } public boolean isSpecialSessionMgr () { return m_bSpecialSessionMgr; } /** * @param bSpecialSessionMgr * true to set a session manager that allows for * persistent activation and passivation of sessions. * @return this for chaining */ @Nonnull public final JettyStarter setSpecialSessionMgr (final boolean bSpecialSessionMgr) { m_bSpecialSessionMgr = bSpecialSessionMgr; return this; } @Nonnull public Resource getResourceBase () { return m_aResourceBase; } /** * Set the common resource base (directory) from which all web application * resources will be loaded (servlet context root). * * @param sResourceBase * The path. May neither be null nor empty. * @return this for chaining */ @Nonnull public final JettyStarter setResourceBase (@Nonnull @Nonempty final String sResourceBase) { ValueEnforcer.notEmpty (sResourceBase, "ResourceBase"); return setResourceBase (_asRes (sResourceBase)); } /** * Set the common resource base (directory) from which all web application * resources will be loaded (servlet context root). * * @param aResourceBase * The resource. May neither be null nor empty. * @return this for chaining */ @Nonnull public final JettyStarter setResourceBase (@Nonnull final Resource aResourceBase) { ValueEnforcer.notNull (aResourceBase, "ResourceBase"); m_aResourceBase = aResourceBase; return this; } @Nullable public String getWebXmlResource () { return m_sWebXmlResource; } /** * Set the path to WEB-INF/web.xml. If unspecified, the default relative to * the resource base is used. * * @param sWebXmlResource * web.xml resource. May be null. * @return this for chaining. */ @Nonnull public final JettyStarter setWebXmlResource (@Nullable final String sWebXmlResource) { m_sWebXmlResource = sWebXmlResource; return this; } @Nonnull @Nonempty public String getContextPath () { return m_sContextPath; } /** * Set the context path in which the web application should run. By default * this {@link #DEFAULT_CONTEXT_PATH} * * @param sContextPath * The new context path. May neither be null nor empty and * must start with a slash. * @return this for chaining */ @Nonnull public final JettyStarter setContextPath (@Nonnull @Nonempty final String sContextPath) { ValueEnforcer.notEmpty (sContextPath, "sContextPath"); m_sContextPath = sContextPath; return this; } @Nullable public String getContainerIncludeJarPattern () { return m_sContainerIncludeJarPattern; } /** * Set the container JAR pattern to be scanned for annotations. By default * this {@link #DEFAULT_CONTAINER_INCLUDE_JAR_PATTERN} * * @param sContainerIncludeJarPattern * The new container JAR pattern. May be null to use the * default. * @return this for chaining */ @Nonnull public final JettyStarter setContainerIncludeJarPattern (@Nullable final String sContainerIncludeJarPattern) { m_sContainerIncludeJarPattern = sContainerIncludeJarPattern; return this; } @Nullable public String getWebInfIncludeJarPattern () { return m_sWebInfIncludeJarPattern; } @Nonnull public final JettyStarter setWebInfIncludeJarPattern (@Nullable final String sWebInfIncludeJarPattern) { m_sWebInfIncludeJarPattern = sWebInfIncludeJarPattern; return this; } @Nullable public ThreadPool getThreadPool () { return m_aThreadPool; } /** * Set the thread pool to use. * * @param aThreadPool * Thread pool. May be null to use the default thread * pool. * @return this * @since 7.0.6 */ @Nonnull public final JettyStarter setThreadPool (@Nullable final ThreadPool aThreadPool) { m_aThreadPool = aThreadPool; return this; } public boolean isAllowAnnotationBasedConfig () { return m_bAllowAnnotationBasedConfig; } /** * Enable or disable annotation based scanning. By default it is enabled. * Disable it for better performance. * * @param bAllowAnnotationBasedConfig * false to disable it. * @return this * @since 8.0.0 */ @Nonnull public final JettyStarter setAllowAnnotationBasedConfig (final boolean bAllowAnnotationBasedConfig) { m_bAllowAnnotationBasedConfig = bAllowAnnotationBasedConfig; return this; } public boolean isAllowDirectoryListing () { return m_bAllowDirectoryListing; } /** * Enable or disable the listing of Directories. Turned off by default for * security reasons. * * @param bAllowDirectoryListing * true to enable it. * @return this * @since 8.3.7 */ @Nonnull public final JettyStarter setAllowDirectoryListing (final boolean bAllowDirectoryListing) { m_bAllowDirectoryListing = bAllowDirectoryListing; return this; } /** * @return The name of the session cookie or null to use Jetty default. The * default values is {@link #DEFAULT_SESSION_COOKIE_NAME}. */ @Nullable public String getSessionCookieName () { return m_sSessionCookieName; } /** * Set the session cookie name. Default is * {@value #DEFAULT_SESSION_COOKIE_NAME}. When running different applications * ensure to use different names to ensure you can test them in the same * browser in the same session. * * @param sSessionCookieName * New name or null to use Jetty default. * @return this for chaining * @since 8.1.0 */ @Nonnull public final JettyStarter setSessionCookieName (@Nullable final String sSessionCookieName) { m_sSessionCookieName = sSessionCookieName; return this; } /** * Customize * * @param aHttpConfiguration * HTTP configuration * @throws Exception * in case of error * @since 8.4.0 */ @OverrideOnDemand protected void customizeHttpConfiguration (@Nonnull final HttpConfiguration aHttpConfiguration) throws Exception {} /** * Customize * * @param aHttpConnectionFactory * HTTP connection factory * @throws Exception * in case of error * @since 8.4.0 */ @OverrideOnDemand protected void customizeHttpConnectionFactory (@Nonnull final HttpConnectionFactory aHttpConnectionFactory) throws Exception {} /** * Customize * * @param aServerConnector * Server connector * @throws Exception * in case of error */ @OverrideOnDemand protected void customizeServerConnector (@Nonnull final ServerConnector aServerConnector) throws Exception {} /** * Customize * * @param aServer * Server * @throws Exception * in case of error */ @OverrideOnDemand protected void customizeServer (@Nonnull final Server aServer) throws Exception {} /** * Customize * * @param aWebAppCtx * Web application context * @throws Exception * in case of error */ @OverrideOnDemand protected void customizeWebAppCtx (@Nonnull final WebAppContext aWebAppCtx) throws Exception {} /** * Create a new {@link WebAppContext} based on the settings of this class. * * @param sContextPath * The context path to be used. May neither be null nor * empty. * @return The created object. Never null. * @throws Exception * In case of error */ @Nonnull public WebAppContext createWebAppContext (@Nonnull @Nonempty final String sContextPath) throws Exception { ValueEnforcer.notEmpty (sContextPath, "ContextPath"); // getTmpDir is e.g. "/tmp" // Context path (if present) starts with a slash final String sTempDir = SystemProperties.getTmpDir () + FilenameHelper.getAsSecureValidASCIIFilename (sContextPath); final WebAppContext aWebAppCtx = new WebAppContext (); { aWebAppCtx.setBaseResource (m_aResourceBase); aWebAppCtx.setDescriptor (m_sWebXmlResource != null ? m_sWebXmlResource : m_aResourceBase.addPath ("/WEB-INF/web.xml").getName ()); aWebAppCtx.setContextPath (sContextPath); aWebAppCtx.setTempDirectory (new File (sTempDir, m_sDirBaseName + ".webapp")); /* * This line can make a difference between Jetty and Tomcat: True if the * classloader should delegate first to the parentclassloader (standard * java behaviour) or false if the classloader should first try to load * from WEB-INF/lib or WEB-INF/classes (servletspec recommendation). * Default is false or can be set by the systemproperty * org.eclipse.jetty.server.webapp.parentLoaderPriority */ aWebAppCtx.setParentLoaderPriority (true); aWebAppCtx.setThrowUnavailableOnStartupException (true); if (m_sContainerIncludeJarPattern != null) { // http://www.eclipse.org/jetty/documentation/9.4.x/configuring-webapps.html#container-include-jar-pattern // https://github.com/eclipse/jetty.project/issues/680 aWebAppCtx.setAttribute (WebInfConfiguration.CONTAINER_JAR_PATTERN, m_sContainerIncludeJarPattern); } if (m_sWebInfIncludeJarPattern != null) { aWebAppCtx.setAttribute (WebInfConfiguration.WEBINF_JAR_PATTERN, m_sWebInfIncludeJarPattern); } if (m_bAllowAnnotationBasedConfig) { // Important to add the AnnotationConfiguration! aWebAppCtx.setConfigurations (new Configuration [] { new WebInfConfiguration (), new WebXmlConfiguration (), new MetaInfConfiguration (), new FragmentConfiguration (), new AnnotationConfiguration (), new JettyWebXmlConfiguration () }); } // else leave default aWebAppCtx.setInitParameter ("org.eclipse.jetty.servlet.Default.dirAllowed", Boolean.toString (m_bAllowDirectoryListing)); } // Set session store directory to passivate/activate sessions if (m_bSpecialSessionMgr) { final SessionHandler aHdl = new SessionHandler (); final FileSessionDataStore aDataStore = new FileSessionDataStore (); aDataStore.setStoreDir (new File (sTempDir, m_sDirBaseName + ".sessions")); aDataStore.setDeleteUnrestorableFiles (true); final DefaultSessionCache aCache = new DefaultSessionCache (aHdl); aCache.setSessionDataStore (aDataStore); aCache.setRemoveUnloadableSessions (true); aHdl.setSessionCache (aCache); aWebAppCtx.setSessionHandler (aHdl); } // Hack to circumvent API limits - ensure SameSite for Session cookie aWebAppCtx.getSessionHandler ().getSessionCookieConfig ().setComment (HttpCookie.SAME_SITE_STRICT_COMMENT); aWebAppCtx.getSessionHandler ().getSessionCookieConfig ().setHttpOnly (true); if (StringHelper.hasText (m_sSessionCookieName)) aWebAppCtx.getSessionHandler ().setSessionCookie (m_sSessionCookieName); // Customize call customizeWebAppCtx (aWebAppCtx); return aWebAppCtx; } /** * Customize the {@link HandlerList} * * @param aHandlerList * The {@link HandlerList}. Never null. * @throws Exception * in case of error */ protected void customizeHandlerList (@Nonnull final HandlerList aHandlerList) throws Exception {} /** * Callback to be invoked when server successfully finished startup. * * @param aServer * The server that was started. Never null. * @throws Exception * in case of error * @since 7.0.2 */ @OverrideOnDemand protected void onServerStarted (@Nonnull final Server aServer) throws Exception {} /** * Callback to be invoked when server failed startup. * * @param aServer * The server that was started. Never null. * @param t * The exception that occurred * @throws Exception * in case of error * @since 7.0.2 */ @OverrideOnDemand protected void onServerStartFailure (@Nonnull final Server aServer, @Nonnull final Throwable t) throws Exception {} /** * Run Jetty with the provided settings. * * @throws Exception * In case something goes wrong */ public void run () throws Exception { if (System.getSecurityManager () != null) throw new IllegalStateException ("Security Manager is set but not supported - aborting!"); // Create main server final Server aServer = new Server (m_aThreadPool); { final HttpConfiguration aHC = new HttpConfiguration (); aHC.setSendServerVersion (false); aHC.setSendXPoweredBy (false); customizeHttpConfiguration (aHC); final HttpConnectionFactory aHCF = new HttpConnectionFactory (aHC); customizeHttpConnectionFactory (aHCF); // Create connector on Port final ServerConnector aConnector = new ServerConnector (aServer, aHCF); aConnector.setPort (m_nPort); aConnector.setIdleTimeout (30_000); customizeServerConnector (aConnector); aServer.setConnectors (new Connector [] { aConnector }); aServer.setAttribute ("org.eclipse.jetty.server.Request.maxFormContentSize", Integer.valueOf (2 * CGlobal.BYTES_PER_MEGABYTE)); aServer.setAttribute ("org.eclipse.jetty.server.Request.maxFormKeys", Integer.valueOf (20000)); } // Customize call customizeServer (aServer); final WebAppContext aWebAppCtx = createWebAppContext (m_sContextPath); final HandlerList aHandlerList = new HandlerList (); aHandlerList.addHandler (aWebAppCtx); // Allow for additional web app contexts ;-) customizeHandlerList (aHandlerList); aServer.setHandler (aHandlerList); final ServletContextHandler aCtx = aWebAppCtx; // Setting final properties // Stops the server when ctrl+c is pressed (registers to // Runtime.addShutdownHook) aServer.setStopAtShutdown (true); if (false) { // Debug output aServer.setDumpBeforeStop (true); } try { // Starting shutdown listener thread final int nStopPort = m_aStopPort.applyAsInt (m_nPort); // May fail if port is in use if (m_bRunStopMonitor) new InternalJettyStopMonitorThread (nStopPort, m_sStopKey, aServer::stop).start (); // Starting the engines: aServer.start (); LOGGER.info ("Started Jetty" + ":" + m_nPort + ":" + nStopPort + " " + m_sAppName); // Callback onServerStarted (aServer); Runtime.getRuntime ().addShutdownHook (new Thread ( () -> { try { aServer.stop (); aServer.join (); } catch (final InterruptedException ex) { Thread.currentThread ().interrupt (); LOGGER.error ("ShutdownHook of JettyStarter has been interrupted!"); } catch (final Exception ex) { LOGGER.error ("Exception in ShutdownHook of JettyStarter!", ex); } })); } catch (final Exception ex) { // Do not throw something here, in case some exception occurs in finally // code LOGGER.error ("Failed to start Jetty " + m_sAppName + "!", ex); // Callback onServerStartFailure (aServer, ex); } finally { if (aCtx.isFailed ()) { LOGGER.error ("Failed to start Jetty " + m_sAppName + " - stopping server!"); try { // Throws an Exception e.g. in log4j2 2.5 aServer.stop (); LOGGER.error ("Failed to start Jetty " + m_sAppName + " - stopped server!"); } catch (final Exception ex) { LOGGER.error ("Error stopping Jetty " + m_sAppName + " after startup errors!", ex); } } else if (!aServer.isFailed ()) { // Running the server! aServer.join (); } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy