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

com.github.bordertech.wcomponents.lde.TestServlet Maven / Gradle / Ivy

There is a newer version: 1.5.36
Show newest version
package com.github.bordertech.wcomponents.lde;

import com.github.bordertech.wcomponents.servlet.WServlet;
import com.github.bordertech.wcomponents.util.Config;
import com.github.bordertech.wcomponents.util.SystemException;
import java.io.IOException;
import java.net.BindException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.bio.SocketConnector;
import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
import org.eclipse.jetty.webapp.WebAppContext;

/**
 * Enables WComponents to be run in a LDE environment.
 *
 * @author James Gifford
 * @since 1.0.0
 */
public abstract class TestServlet extends WServlet {

	/**
	 * The logger instance for this class.
	 */
	private static final Log LOG = LogFactory.getLog(TestServlet.class);

	/**
	 * This configuration parameter allows the developer to configure the LDE to use a different set of servlets.
	 */
	private static final String WEBDOCS_DIR_PARAM = "bordertech.wcomponents.lde.webdocs.dir";

	/**
	 * This configuration parameter allows the developer to configure the LDE to use an external theme.
	 */
	private static final String WEBDOCS_THEME_DIR_PARAM = "bordertech.wcomponents.lde.theme.dir";

	/**
	 * This configuration parameter allows the developer to configure the LDE to set a resources directory.
	 */
	private static final String WEBDOCS_RESOURCE_DIR_PARAM = "bordertech.wcomponents.lde.resource.dir";

	/**
	 * This configuration parameter sets which port the LDE runs on.
	 */
	private static final String JETTY_PORT_PARAM = "bordertech.wcomponents.lde.server.port";

	/**
	 * This configuration parameter sets up the Jetty realm file for authenticated access.
	 */
	private static final String JETTY_REALM_FILE_PARAM = "bordertech.wcomponents.lde.server.realm.file";

	/**
	 * This configuration parameter sets up the Jetty realm file for authenticated access.
	 */
	private static final String ENABLE_SHUTDOWN_PARAM = "bordertech.wcomponents.lde.shutdown.enabled";

	/**
	 * This configuration parameter sets the Jetty session timeout interval.
	 */
	private static final String SESSION_TIMEOUT_PARAM = "bordertech.wcomponents.lde.session.inactive.interval";

	/**
	 * The default port to run the LDE on.
	 */
	private static final int DEFAULT_PORT = 8080;

	private final List union = new ArrayList<>();

	/**
	 * The Jetty server instance for this VM. Only one LDE can be run per VM.
	 */
	private static Server server;

	/**
	 * The URL where this servlet can be accessed from.
	 */
	private String url;

	/**
	 * Runs the server.
	 *
	 * @throws Exception if the LDE fails to start.
	 */
	public void run() throws Exception {
		synchronized (TestServlet.class) {
			if (server != null) {
				stop();
			}

			server = new Server();
		}

		SocketConnector connector = new SocketConnector();
		connector.setMaxIdleTime(0);
		connector.setPort(getLdePort());
		server.addConnector(connector);

		WebAppContext webapp = createWebApp(server);

		try {
			server.start();
		} catch (BindException e) {
			if (isShutdownEnabled()) {
				// The port is in use, possibly by another LDE instance.
				// Attempt to shut down the other LDE and start up again.
				LOG.info("Attempting remote shutdown of existing LDE");
				shutDown();

				Thread.sleep(100); // give the OS a chance to release the port
				server.start();
			} else {
				throw e;
			}
		} catch (Exception e) {
			// Failed to start the server
			server = null;
			throw e;
		}

		// We have to set the timeout after the server is started, as Jetty reads
		// the value from webdefault.xml during start-up.
		int timeout = Config.getInstance().getInt(SESSION_TIMEOUT_PARAM, 0);

		if (timeout > 0) {
			webapp.getSessionHandler().getSessionManager().setMaxInactiveInterval(timeout);
		}

		// Server started successfully, log the URL for the LDE.
		url = "http://localhost:" + connector.getLocalPort() + "/app";
		LOG.info("URL  ==>  " + url);
	}

	@Override
	public void service(final ServletRequest req, final ServletResponse res) throws ServletException,
			IOException {
		if (req.getParameter("lde.shutdown") != null && isShutdownEnabled()) {
			LOG.info("Received LDE shutdown request, stopping server.");
			res.getOutputStream().close();

			try {
				stop();
			} catch (InterruptedException e) {
				LOG.error("Failed to shut down LDE server", e);
			}

			return;
		}

		super.service(req, res);
	}

	/**
	 * Retrieves the port number which the LDE should run on.
	 *
	 * @return the LDE port.
	 */
	private static int getLdePort() {
		int port = DEFAULT_PORT;

		try {
			port = Config.getInstance().getInt(JETTY_PORT_PARAM, DEFAULT_PORT);
		} catch (Exception ignored) {
			LOG.error("Failed to read parameter " + JETTY_PORT_PARAM);
		}

		return port;
	}

	/**
	 * Attempts to shutdown an existing TestServlet, possibly in another VM.
	 */
	protected void shutDown() {
		try {
			URL shutdownUrl = new URL("http://localhost:" + getLdePort()
					+ "/app?lde.shutdown=true");

			HttpURLConnection conn = (HttpURLConnection) shutdownUrl.openConnection();
			conn.getResponseCode();
		} catch (ConnectException expected) {
			// This will be thrown if either the other LDE is not running,
			// or it was running and this connection has caused it to terminate.
			return;
		} catch (Exception e) {
			LOG.error("Failed to shut down other LDE instance", e);
		}
	}

	/**
	 * Creates the Web app context to use in the LDE. The context will be registered with the given server.
	 *
	 * @param srv the Jetty server.
	 * @return the newly created Web app context.
	 * @throws Exception an exception
	 */
	protected WebAppContext createWebApp(final Server srv) throws Exception {
		String[] webdocs = getWebdocsDir();
		String[] themeWebdocs = getThemeWebdocs();
		String[] resourceDirs = getResourceDir();

		if (webdocs != null) {
			for (int i = 0; i < webdocs.length; i++) {
				union.add(Resource.newResource(webdocs[i]));
			}
		}

		if (themeWebdocs != null) {
			for (int i = 0; i < themeWebdocs.length; i++) {
				union.add(Resource.newResource(themeWebdocs[i]));
			}
		}

		if (resourceDirs != null) {
			for (int i = 0; i < resourceDirs.length; i++) {
				union.add(Resource.newResource(resourceDirs[i]));
			}
		}

		HandlerCollection handlers = new HandlerCollection();
		WebAppContext webapp = null;

		// If there is no external web.xml override, register the default servlets
		if (webdocs == null) {
			webapp = new WebAppContext();
			webapp.setContextPath("/");
			registerServlets(webapp);
		} else {
			webapp = new WebAppContext(webdocs[0], "/");
		}

		// Must have at least one resource
		if (union.isEmpty()) {
			webapp.setResourceBase(".");
		} else {
			webapp.setBaseResource(
					new ResourceCollection(union.toArray(new Resource[union.size()])));
		}

		webapp.addServlet(getClass().getName(), "/app/*");

		// This is required if projects define their own web.xml,
		// we still need to serve up the theme from inside a jar
		// file using the theme servlet.
		if (themeWebdocs == null) {
			WebAppContext themeWebapp = new WebAppContext();
			themeWebapp.setContextPath("/theme");
			themeWebapp.addServlet("com.github.bordertech.wcomponents.servlet.ThemeServlet", "/*");
			themeWebapp.setResourceBase(".");
			handlers.addHandler(themeWebapp);
		} else {
			WebAppContext themeWebapp = new WebAppContext();
			themeWebapp.setContextPath("/theme");
			themeWebapp.setResourceBase(themeWebdocs[0]);
			handlers.addHandler(themeWebapp);
		}

		// Initialise security
		String realmFile = Config.getInstance().getString(JETTY_REALM_FILE_PARAM);

		if (realmFile != null) {
			HashLoginService loginService = new HashLoginService("LdeRunner", realmFile);
			webapp.getSecurityHandler().setLoginService(loginService);
		}

		handlers.addHandler(webapp);
		srv.setHandler(handlers);

		return webapp;
	}

	/**
	 * Stops the server.
	 *
	 * @throws java.lang.InterruptedException an interrupted exception
	 */
	public void stop() throws InterruptedException {
		synchronized (TestServlet.class) {
			if (server != null) {
				try {
					server.stop();
				} catch (Exception e) {
					LOG.warn("Failed to stop server", e);
				}

				server = null;
			}
		}
	}

	/**
	 * @return the URL where this servlet can be accessed from.
	 */
	protected String getUrl() {
		return url;
	}

	/**
	 * @param jarname the jar name to add
	 */
	public void addIndirectJar(final String jarname) {

		try (JarFile jarfile = new JarFile(jarname)) {
			Manifest man = jarfile.getManifest();
			Attributes atts = man.getMainAttributes();
			String jarlist = atts.getValue("Class-Path");
			StringTokenizer tokenizer = new StringTokenizer(jarlist, " ", false);
			while (tokenizer.hasMoreTokens()) {
				String tokenUrl = tokenizer.nextToken();
				union.add(Resource.newResource("jar:" + tokenUrl + "!/"));
				LOG.info("Added webdocs at " + tokenUrl);
			}
		} catch (IOException ex) {
			throw new SystemException("Could handle indirect jar " + jarname,
					ex);
		}
	}

	/**
	 * @return the webdocs directory, or null if not defined.
	 */
	protected String[] getWebdocsDir() {
		String[] docs = Config.getInstance().getStringArray(WEBDOCS_DIR_PARAM);
		return docs == null || docs.length == 0 ? null : docs;
	}

	/**
	 * @return the theme webdocs directory, or null if not defined.
	 */
	protected String[] getThemeWebdocs() {
		String[] docs = Config.getInstance().getStringArray(WEBDOCS_THEME_DIR_PARAM);
		return docs == null || docs.length == 0 ? null : docs;
	}

	/**
	 * @return the resource directory, or null if not defined.
	 */
	protected String[] getResourceDir() {
		String[] docs = Config.getInstance().getStringArray(WEBDOCS_RESOURCE_DIR_PARAM);
		return docs == null || docs.length == 0 ? null : docs;
	}

	/**
	 * Override service in order to support persistant sessions.
	 *
	 * @param request the request being processed
	 * @param response the response
	 * @throws javax.servlet.ServletException a servlet exception
	 * @throws java.io.IOException an IO exception
	 */
	@Override
	protected void service(final HttpServletRequest request, final HttpServletResponse response)
			throws ServletException, IOException {
		// This is a new session, do we want to load a persisted session?
		if (request.getSession(false) == null && LdeSessionUtil.isLoadPersistedSessionEnabled()) {
			getUI(request); // need to ensure that the UI has been loaded
			LdeSessionUtil.deserializeSessionAttributes(request.getSession(true));
		}

		super.service(request, response);

		// Persist the session if necessary.
		if (LdeSessionUtil.isPersistSessionEnabled()) {
			LdeSessionUtil.serializeSessionAttributes(request.getSession());
		}
	}

	/**
	 * Subclasses may override this to register additional servlets with the server.
	 *
	 * @param webapp the webapp to register the servlets with.
	 */
	protected void registerServlets(final WebAppContext webapp) {
		//webapp.addServlet("themeServlet", "/theme/*",
		// "com.github.bordertech.wcomponents.lde.LdeThemeServlet");
	}

	/**
	 * Indicates whether remote LDE shutdown is enabled.
	 *
	 * @return true if shutdown is enabled, false otherwise.
	 */
	protected boolean isShutdownEnabled() {
		return Config.getInstance().getBoolean(ENABLE_SHUTDOWN_PARAM, false);
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy