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

org.specrunner.jetty.PluginStartJetty Maven / Gradle / Ivy

There is a newer version: 1.5.17
Show newest version
/*
    SpecRunner - Acceptance Test Driven Development Tool
    Copyright (C) 2011-2016  Thiago Santos

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see 
 */
package org.specrunner.jetty;

import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ShutdownHandler;
import org.eclipse.jetty.server.session.HashSessionManager;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.preventers.AppContextLeakPreventer;
import org.eclipse.jetty.webapp.WebAppContext;
import org.eclipse.jetty.xml.XmlConfiguration;
import org.specrunner.SRServices;
import org.specrunner.context.IContext;
import org.specrunner.context.IDestructable;
import org.specrunner.features.IFeatureManager;
import org.specrunner.plugins.ActionType;
import org.specrunner.plugins.ENext;
import org.specrunner.plugins.PluginException;
import org.specrunner.plugins.core.AbstractPluginScoped;
import org.specrunner.plugins.type.Command;
import org.specrunner.result.IResultSet;
import org.specrunner.result.status.Success;
import org.specrunner.reuse.IReusable;
import org.specrunner.reuse.IReuseManager;
import org.specrunner.reuse.core.AbstractReusable;
import org.specrunner.util.UtilLog;

/**
 * Starts a jetty server based on a jetty.xml file.
 * 
 * @author Thiago Santos
 * 
 */
public class PluginStartJetty extends AbstractPluginScoped {

    /**
     * Default server name.
     */
    public static final String SERVER_NAME = "jettyName";
    /**
     * Lock object used to guarantee port exclusion on multi-thread executions.
     */
    private static Object lock = new Object();

    /**
     * Feature to set configuration file.
     */
    public static final String FEATURE_FILE = PluginStartJetty.class.getName() + ".file";
    /**
     * The file.
     */
    private String file;

    /**
     * Feature to enable server allocate a dynamic port.
     */
    public static final String FEATURE_DYNAMIC = PluginStartJetty.class.getName() + ".dynamic";
    /**
     * Set dynamic start up. Default is 'true'.
     */
    private Boolean dynamic = true;

    /**
     * Feature to set server port.
     */
    public static final String FEATURE_PORT = PluginStartJetty.class.getName() + ".port";
    /**
     * The port to be used.
     */
    private Integer port;

    /**
     * Feature to set reusable "jetties".
     */
    public static final String FEATURE_REUSE = PluginStartJetty.class.getName() + ".reuse";
    /**
     * The reuse status. Default is 'false'.
     */
    private Boolean reuse = false;

    /**
     * Feature to set class loader.
     */
    public static final String FEATURE_CLASSLOADER = PluginStartJetty.class.getName() + ".classloader";
    /**
     * The class loader adjust to current Thread. Default is 'true'.
     */
    private Boolean classloader = true;

    /**
     * Default constructor.
     */
    public PluginStartJetty() {
        setName(SERVER_NAME);
    }

    /**
     * Gets the configuration file.
     * 
     * @return The configuration file.
     */
    public String getFile() {
        return file;
    }

    /**
     * Set the configuration file.
     * 
     * @param file
     *            The file.
     */
    public void setFile(String file) {
        this.file = file;
    }

    /**
     * Get dynamic status.
     * 
     * @return true, if dynamic port allocation is enable, false, otherwise.
     */
    public Boolean getDynamic() {
        return dynamic;
    }

    /**
     * Set dynamic mode.
     * 
     * @param dynamic
     *            The dynamic status.
     */
    public void setDynamic(Boolean dynamic) {
        this.dynamic = dynamic;
    }

    /**
     * Get port value.
     * 
     * @return The port value.
     */
    public Integer getPort() {
        return port;
    }

    /**
     * Set expected port.
     * 
     * @param port
     *            The port.
     */
    public void setPort(Integer port) {
        this.port = port;
    }

    /**
     * Get reusable Jetty.
     * 
     * @return The reuse status.
     */
    public Boolean getReuse() {
        return reuse;
    }

    /**
     * Set reuse status.
     * 
     * @param reuse
     *            true, for reuse, false otherwise.
     */
    public void setReuse(Boolean reuse) {
        this.reuse = reuse;
    }

    /**
     * Get the classloader setup.
     * 
     * @return The classloader state.
     */
    public Boolean getClassloader() {
        return classloader;
    }

    /**
     * Enable the revertion of classloader to current thread.
     * 
     * @param classloader
     *            The classloader flag.
     */
    public void setClassloader(Boolean classloader) {
        this.classloader = classloader;
    }

    @Override
    public ActionType getActionType() {
        return Command.INSTANCE;
    }

    @Override
    public void initialize(IContext context) throws PluginException {
        super.initialize(context);
        IFeatureManager fm = SRServices.getFeatureManager();
        if (file == null) {
            fm.set(FEATURE_FILE, this);
        }
        fm.set(FEATURE_DYNAMIC, this);
        if (port == null) {
            fm.set(FEATURE_PORT, this);
        }
        fm.set(FEATURE_REUSE, this);
    }

    @Override
    public ENext doStart(IContext context, IResultSet result) throws PluginException {
        synchronized (lock) {
            try {
                Log.getRootLogger().setDebugEnabled(true);
                if (file == null) {
                    throw new PluginException("Jetty file name must be set using attribute 'file'.");
                }

                if (UtilLog.LOG.isInfoEnabled()) {
                    UtilLog.LOG.info("Jetty version -> " + Server.getVersion() + ".");
                }

                IReuseManager reusables = SRServices.get(IReuseManager.class);
                if (reuse) {
                    Map cfg = new HashMap();
                    cfg.put(getFileForJettyName(getName()), file);
                    IReusable reusable = reusables.get(getName());
                    if (reusable != null && reusable.canReuse(cfg)) {
                        reusable.reset();
                        saveGlobal(context, getName(), reusable.getObject());
                        result.addResult(Success.INSTANCE, context.peek());
                        if (UtilLog.LOG.isInfoEnabled()) {
                            UtilLog.LOG.info("Jetty (" + getName() + "/" + Server.getVersion() + ") with " + file + " reused.");
                        }
                        return ENext.DEEP;
                    }
                }

                final Server server = createServer();

                LocalSessionManager sm = setSessionManager(server);

                if (dynamic) {
                    if (UtilLog.LOG.isInfoEnabled()) {
                        UtilLog.LOG.info("Jetty dynamic port lookup.");
                    }
                    scanAvailablePort(server);
                }

                perform(server);

                waitForStart(server);

                save(context, server);

                if (reuse) {
                    if (UtilLog.LOG.isInfoEnabled()) {
                        UtilLog.LOG.info("Jetty reuse enabled.");
                    }
                    reusables.put(getName(), new ReusableJetty(getName(), server, sm));
                }
                result.addResult(Success.INSTANCE, context.peek());
                if (!reuse && UtilLog.LOG.isInfoEnabled()) {
                    UtilLog.LOG.info("Jetty started and ready.");
                }
            } catch (Exception e) {
                throw new PluginException(e);
            }
        }
        return ENext.DEEP;
    }

    /**
     * Save server to context.
     * 
     * @param context
     *            A context.
     * @param server
     *            A server.
     */
    protected void save(IContext context, final Server server) {
        if (reuse) {
            saveGlobal(context, getName(), server);
        } else {
            saveGlobal(context, getName(), new IDestructable() {

                @Override
                public Object getObject() {
                    return server;
                }

                @Override
                public void destroy() {
                    try {
                        server.stop();
                    } catch (Exception e) {
                        if (UtilLog.LOG.isDebugEnabled()) {
                            UtilLog.LOG.debug(e.getMessage(), e);
                        }
                    }
                }
            });
        }
    }

    /**
     * Creates the server from configuration file.
     * 
     * @return The server.
     * @throws Exception
     *             On creation errors.
     */
    protected Server createServer() throws Exception {
        InputStream config = PluginStartJetty.class.getResourceAsStream(file);
        if (config == null) {
            throw new PluginException("Jetty file '" + file + "' not found.");
        }
        XmlConfiguration configuration = new XmlConfiguration(config);
        config.close();
        final Server server = (Server) configuration.configure();
        server.addBean(new AppContextLeakPreventer());
        setShutdownManager(server);
        getPortFromServer(server);
        setClassloader(server);
        return server;
    }

    /**
     * Set shutdown manager.
     * 
     * @param server
     *            The server
     */
    protected void setShutdownManager(final Server server) {
        // if shutdown is request die
        server.setStopAtShutdown(true);
        // cannot shutdown JVM, leave it to test programmer.
        ShutdownHandler sdh = server.getChildHandlerByClass(ShutdownHandler.class);
        if (sdh != null) {
            sdh.setExitJvm(false);
            if (UtilLog.LOG.isInfoEnabled()) {
                UtilLog.LOG.info("Jetty System.exit(..) disabled.");
            }
        }
    }

    /**
     * Recover port information from a server previously created.
     * 
     * @param server
     *            The server.
     */
    protected void getPortFromServer(final Server server) {
        for (Connector c : server.getConnectors()) {
            if (c instanceof ServerConnector) {
                port = ((ServerConnector) c).getPort();
                break;
            }
        }
    }

    /**
     * Set Jetty classloader.
     * 
     * @param server
     *            The server.
     */
    protected void setClassloader(final Server server) {
        WebAppContext web = server.getChildHandlerByClass(WebAppContext.class);
        if (web != null && classloader) {
            web.setClassLoader(Thread.currentThread().getContextClassLoader());
        }
    }

    /**
     * Sets the session manager. for reusable server the session manager must
     * invalidate all previous sessions.
     * 
     * @param server
     *            The server.
     * @return The session manager used.
     */
    protected LocalSessionManager setSessionManager(final Server server) {
        LocalSessionManager sm = null;
        if (reuse) {
            WebAppContext web = server.getChildHandlerByClass(WebAppContext.class);
            if (web != null) {
                SessionHandler h = web.getSessionHandler();
                sm = new LocalSessionManager();
                h.setSessionManager(sm);
                if (UtilLog.LOG.isInfoEnabled()) {
                    UtilLog.LOG.info("Jetty LocalSessionManager added -> " + sm + ".");
                }
            }
        }
        return sm;
    }

    /**
     * Scan for a available port to set into server.
     * 
     * @param server
     *            The server.
     * @throws PluginException
     *             On setting errors.
     */
    protected void scanAvailablePort(final Server server) throws PluginException {
        boolean available = false;
        final int tries = 1000;
        for (int i = port; !available && i < port + tries; i++) {
            InputStream in = null;
            Socket sock = null;
            try {
                sock = new Socket("localhost", i);
                in = sock.getInputStream();
            } catch (Exception e) {
                port = i;
                available = true;
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (Exception e) {
                        if (UtilLog.LOG.isTraceEnabled()) {
                            UtilLog.LOG.trace(e.getMessage(), e);
                        }
                    }
                }
                if (sock != null) {
                    try {
                        sock.close();
                    } catch (Exception e) {
                        if (UtilLog.LOG.isTraceEnabled()) {
                            UtilLog.LOG.trace(e.getMessage(), e);
                        }
                    }
                }
            }
        }
        if (!available) {
            throw new PluginException("No available port from '" + (port - tries) + "' to '" + port + "'.");
        }
        if (UtilLog.LOG.isInfoEnabled()) {
            UtilLog.LOG.info("Jetty port '" + port + "' available.");
        }
        for (Connector c : server.getConnectors()) {
            if (c instanceof ServerConnector) {
                ((ServerConnector) c).setPort(port);
                if (UtilLog.LOG.isInfoEnabled()) {
                    UtilLog.LOG.info("Jetty port set to '" + port + "'.");
                }
                break;
            }
        }
    }

    /**
     * Hook for customized setups.
     * 
     * @param server
     *            The server object.
     */
    public void perform(Server server) {
        // perform something.
    }

    /**
     * Start an wait for server start.
     * 
     * @param server
     *            The server.
     * @throws Exception
     *             On initialization errors.
     */
    protected void waitForStart(final Server server) throws Exception {
        server.start();
        while (!server.isRunning()) {
            if (UtilLog.LOG.isInfoEnabled()) {
                UtilLog.LOG.info("Waiting Jetty start.");
            }
        }
    }

    /**
     * Gets Jetty file for a given Jetty.
     * 
     * @param jettyName
     *            The Jetty name.
     * @return The configuration file.
     */
    protected String getFileForJettyName(String jettyName) {
        return "file_" + jettyName;
    }

    /**
     * A implementation that exposes the invalidateSessions method.
     * 
     * @author Thiago Santos
     * 
     */
    private final class LocalSessionManager extends HashSessionManager {

        @Override
        public void shutdownSessions() {
            try {
                super.shutdownSessions();
                if (UtilLog.LOG.isInfoEnabled()) {
                    UtilLog.LOG.info("Sessions removed.");
                }
            } catch (Exception e) {
                if (UtilLog.LOG.isDebugEnabled()) {
                    UtilLog.LOG.debug(e.getMessage(), e);
                }
            }
        }
    }

    /**
     * Reusable server resource.
     * 
     * @author Thiago Santos
     * 
     */
    protected final class ReusableJetty extends AbstractReusable {
        /**
         * Session manager.
         */
        private final LocalSessionManager sessionManager;

        /**
         * Creates a reusable server package.
         * 
         * @param name
         *            The server name.
         * @param server
         *            The server instance.
         * @param sessionManager
         *            The session manager.
         */
        protected ReusableJetty(String name, Server server, LocalSessionManager sessionManager) {
            super(name, server);
            this.sessionManager = sessionManager;
        }

        @Override
        public boolean canReuse(Map extra) {
            Object obj = extra.get(getFileForJettyName(getName()));
            return obj != null && obj.equals(file);
        }

        @Override
        public void reset() {
            if (UtilLog.LOG.isInfoEnabled()) {
                UtilLog.LOG.info("Jetty recycling '" + getObject() + "'.");
            }
            for (Connector c : getObject().getConnectors()) {
                if (c instanceof ServerConnector) {
                    if (UtilLog.LOG.isInfoEnabled()) {
                        UtilLog.LOG.info("Jetty port listening on '" + ((ServerConnector) c).getPort() + "'.");
                    }
                    break;
                }
            }
            if (sessionManager != null) {
                sessionManager.shutdownSessions();
            }
            getObject().clearAttributes();
            if (UtilLog.LOG.isInfoEnabled()) {
                UtilLog.LOG.info("Jetty '" + getName() + "' attributes cleared.");
            }
        }

        @Override
        public void release() {
            try {
                getObject().stop();
            } catch (Exception e) {
                if (UtilLog.LOG.isDebugEnabled()) {
                    UtilLog.LOG.debug(e.getMessage(), e);
                }
            } finally {
                if (UtilLog.LOG.isInfoEnabled()) {
                    UtilLog.LOG.info("Jetty '" + getName() + "' shutdown.");
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy