org.jumpmind.symmetric.SymmetricWebServer.java Source code

Java tutorial

Introduction

Here is the source code for org.jumpmind.symmetric.SymmetricWebServer.java

Source

/**
 * Licensed to JumpMind Inc under one or more contributor
 * license agreements.  See the NOTICE file distributed
 * with this work for additional information regarding
 * copyright ownership.  JumpMind Inc licenses this file
 * to you under the GNU General Public License, version 3.0 (GPLv3)
 * (the "License"); you may not use this file except in compliance
 * with the License.
 *
 * You should have received a copy of the GNU General Public License,
 * version 3.0 (GPLv3) along with this library; if not, see
 * <http://www.gnu.org/licenses/>.
 *
 * 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 org.jumpmind.symmetric;

import static org.apache.commons.lang.StringUtils.isNotBlank;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Enumeration;

import javax.management.Attribute;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import mx4j.tools.adaptor.http.HttpAdaptor;
import mx4j.tools.adaptor.http.XSLTProcessor;

import org.apache.commons.lang.StringUtils;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.HashLoginService;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.session.AbstractSession;
import org.eclipse.jetty.server.session.HashSessionManager;
import org.eclipse.jetty.server.session.HashedSession;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Password;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.webapp.WebAppContext;
import org.jumpmind.properties.TypedProperties;
import org.jumpmind.security.ISecurityService;
import org.jumpmind.security.SecurityConstants;
import org.jumpmind.security.SecurityServiceFactory;
import org.jumpmind.security.SecurityServiceFactory.SecurityServiceType;
import org.jumpmind.symmetric.common.ServerConstants;
import org.jumpmind.symmetric.common.SystemConstants;
import org.jumpmind.symmetric.transport.TransportManagerFactory;
import org.jumpmind.symmetric.web.ServletUtils;
import org.jumpmind.symmetric.web.SymmetricEngineHolder;
import org.jumpmind.symmetric.web.WebConstants;
import org.jumpmind.symmetric.web.rest.RestService;
import org.jumpmind.util.AppUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

/**
 * Start up SymmetricDS through an embedded Jetty instance.
 *
 * @see SymmetricLauncher#main(String[])
 */
public class SymmetricWebServer {

    protected static final Logger log = LoggerFactory.getLogger(SymmetricWebServer.class);

    protected static final String DEFAULT_WEBAPP_DIR = System.getProperty(SystemConstants.SYSPROP_WEB_DIR,
            AppUtils.getSymHome() + "/web");

    public static final String DEFAULT_HTTP_PORT = System.getProperty(SystemConstants.SYSPROP_DEFAULT_HTTP_PORT,
            "31415");

    public static final String DEFAULT_JMX_PORT = System.getProperty(SystemConstants.SYSPROP_DEFAULT_JMX_PORT,
            "31416");

    public static final String DEFAULT_HTTPS_PORT = System.getProperty(SystemConstants.SYSPROP_DEFAULT_HTTPS_PORT,
            "31417");

    public static final int DEFAULT_MAX_IDLE_TIME = 7200000;

    /**
     * The type of HTTP connection to create for this SymmetricDS web server
     */
    public enum Mode {
        HTTP, HTTPS, MIXED;
    }

    private Server server;

    private WebAppContext webapp;

    protected boolean join = true;

    protected String webHome = "/";

    protected int maxIdleTime = DEFAULT_MAX_IDLE_TIME;

    protected boolean httpEnabled = true;

    protected int httpPort = Integer.parseInt(DEFAULT_HTTP_PORT);

    protected boolean httpsEnabled = false;

    protected int httpsPort = -1;

    protected boolean jmxEnabled = true;

    protected int jmxPort = Integer.parseInt(DEFAULT_JMX_PORT);

    protected String basicAuthUsername = null;

    protected String basicAuthPassword = null;

    protected String propertiesFile = null;

    protected String host = null;

    protected boolean noNio = false;

    protected boolean noDirectBuffer = false;

    protected String webAppDir = DEFAULT_WEBAPP_DIR;

    protected String name = "SymmetricDS";

    protected String httpSslVerifiedServerNames = "all";

    protected boolean allowSelfSignedCerts = true;

    public SymmetricWebServer() {
        this(null, DEFAULT_WEBAPP_DIR);
    }

    public SymmetricWebServer(String propertiesUrl) {
        this(propertiesUrl, DEFAULT_WEBAPP_DIR);
    }

    public SymmetricWebServer(int maxIdleTime, String propertiesUrl) {
        this(propertiesUrl, DEFAULT_WEBAPP_DIR);
        this.maxIdleTime = maxIdleTime;
    }

    public SymmetricWebServer(String webDirectory, int maxIdleTime, String propertiesUrl, boolean join,
            boolean noNio, boolean noDirectBuffer) {
        this(propertiesUrl, webDirectory);
        this.maxIdleTime = maxIdleTime;
        this.join = join;
        this.noDirectBuffer = noDirectBuffer;
        this.noNio = noNio;
    }

    public SymmetricWebServer(String propertiesUrl, String webappDir) {
        this.propertiesFile = propertiesUrl;
        this.webAppDir = webappDir;
        initFromProperties();
    }

    protected void initFromProperties() {

        try {
            Class.forName(AbstractCommandLauncher.class.getName());
        } catch (ClassNotFoundException e) {
        }

        TypedProperties serverProperties = new TypedProperties(System.getProperties());
        httpEnabled = serverProperties.is(ServerConstants.HTTP_ENABLE,
                Boolean.parseBoolean(System.getProperty(ServerConstants.HTTP_ENABLE, "true")));
        httpsEnabled = serverProperties.is(ServerConstants.HTTPS_ENABLE,
                Boolean.parseBoolean(System.getProperty(ServerConstants.HTTPS_ENABLE, "true")));
        jmxEnabled = serverProperties.is(ServerConstants.JMX_HTTP_ENABLE,
                Boolean.parseBoolean(System.getProperty(ServerConstants.JMX_HTTP_ENABLE, "true")));
        httpPort = serverProperties.getInt(ServerConstants.HTTP_PORT,
                Integer.parseInt(System.getProperty(ServerConstants.HTTP_PORT, "" + httpPort)));
        httpsPort = serverProperties.getInt(ServerConstants.HTTPS_PORT,
                Integer.parseInt(System.getProperty(ServerConstants.HTTPS_PORT, "" + httpsPort)));
        jmxPort = serverProperties.getInt(ServerConstants.JMX_HTTP_PORT,
                Integer.parseInt(System.getProperty(ServerConstants.JMX_HTTP_PORT, "" + jmxPort)));
        host = serverProperties.get(ServerConstants.HOST_BIND_NAME,
                System.getProperty(ServerConstants.HOST_BIND_NAME, host));
        httpSslVerifiedServerNames = serverProperties.get(ServerConstants.HTTPS_VERIFIED_SERVERS,
                System.getProperty(ServerConstants.HTTPS_VERIFIED_SERVERS, httpSslVerifiedServerNames));
        allowSelfSignedCerts = serverProperties.is(ServerConstants.HTTPS_ALLOW_SELF_SIGNED_CERTS,
                Boolean.parseBoolean(System.getProperty(ServerConstants.HTTPS_ALLOW_SELF_SIGNED_CERTS,
                        "" + allowSelfSignedCerts)));

    }

    public SymmetricWebServer start(int httpPort, int jmxPort, String propertiesUrl) throws Exception {
        this.propertiesFile = propertiesUrl;
        return start(httpPort, jmxPort);
    }

    public SymmetricWebServer start() throws Exception {
        if (httpPort > 0 && httpsPort > 0 && httpEnabled && httpsEnabled) {
            return startMixed(httpPort, httpsPort, jmxPort);
        } else if (httpPort > 0 && httpEnabled) {
            return start(httpPort, jmxPort);
        } else if (httpsPort > 0 && httpsEnabled) {
            return startSecure(httpsPort, jmxPort);
        } else {
            throw new IllegalStateException(
                    "Either an http or https port needs to be set before starting the server.");
        }
    }

    public SymmetricWebServer start(int httpPort) throws Exception {
        return start(httpPort, 0, httpPort + 1, Mode.HTTP);
    }

    public SymmetricWebServer start(int httpPort, int jmxPort) throws Exception {
        return start(httpPort, 0, jmxPort, Mode.HTTP);
    }

    public SymmetricWebServer startSecure(int httpsPort, int jmxPort) throws Exception {
        return start(0, httpsPort, jmxPort, Mode.HTTPS);
    }

    public SymmetricWebServer startMixed(int httpPort, int secureHttpPort, int jmxPort) throws Exception {
        return start(httpPort, secureHttpPort, jmxPort, Mode.MIXED);
    }

    public SymmetricWebServer start(int httpPort, int securePort, int httpJmxPort, Mode mode) throws Exception {

        TransportManagerFactory.initHttps(httpSslVerifiedServerNames, allowSelfSignedCerts);

        // indicate to the app that we are in stand alone mode
        System.setProperty(SystemConstants.SYSPROP_STANDALONE_WEB, "true");

        server = new Server();

        server.setConnectors(getConnectors(server, httpPort, securePort, mode));
        setupBasicAuthIfNeeded(server);

        webapp = new WebAppContext();
        webapp.setParentLoaderPriority(true);
        webapp.setConfigurationDiscovered(true);
        webapp.setContextPath(webHome);
        webapp.setWar(webAppDir);
        webapp.setResourceBase(webAppDir);
        // webapp.addServlet(DefaultServlet.class, "/*");

        SessionManager sm = new SessionManager();
        webapp.getSessionHandler().setSessionManager(sm);

        webapp.getServletContext().getContextHandler().setMaxFormContentSize(Integer
                .parseInt(System.getProperty("org.eclipse.jetty.server.Request.maxFormContentSize", "800000")));
        webapp.getServletContext().getContextHandler().setMaxFormKeys(
                Integer.parseInt(System.getProperty("org.eclipse.jetty.server.Request.maxFormKeys", "100000")));
        if (propertiesFile != null) {
            webapp.getServletContext().getContextHandler()
                    .setInitParameter(WebConstants.INIT_SINGLE_SERVER_PROPERTIES_FILE, propertiesFile);
            webapp.getServletContext().getContextHandler()
                    .setInitParameter(WebConstants.INIT_PARAM_MULTI_SERVER_MODE, Boolean.toString(false));
        } else {
            webapp.getServletContext().getContextHandler()
                    .setInitParameter(WebConstants.INIT_PARAM_MULTI_SERVER_MODE, Boolean.toString(true));
        }
        server.setHandler(webapp);

        server.start();

        if (httpJmxPort > 0) {
            registerHttpJmxAdaptor(httpJmxPort);
        }

        if (join) {
            log.info("Joining the web server main thread");
            server.join();
        }

        return this;
    }

    protected ServletContext getServletContext() {
        return webapp != null ? webapp.getServletContext() : null;
    }

    public RestService getRestService() {
        ServletContext servletContext = getServletContext();
        WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
        return rootContext.getBean(RestService.class);
    }

    public ISymmetricEngine getEngine() {
        ISymmetricEngine engine = null;
        ServletContext servletContext = getServletContext();
        if (servletContext != null) {
            SymmetricEngineHolder engineHolder = ServletUtils.getSymmetricEngineHolder(servletContext);
            if (engineHolder != null) {
                if (engineHolder.getEngines().size() == 1) {
                    return engineHolder.getEngines().values().iterator().next();
                } else {
                    throw new IllegalStateException("Could not choose a single engine to return.  There are "
                            + engineHolder.getEngines().size() + " engines configured.");
                }
            }
        }
        return engine;
    }

    public void waitForEnginesToComeOnline(long maxWaitTimeInMs) throws InterruptedException {
        long startTime = System.currentTimeMillis();
        ServletContext servletContext = getServletContext();
        if (servletContext != null) {
            SymmetricEngineHolder engineHolder = ServletUtils.getSymmetricEngineHolder(servletContext);
            while (engineHolder.areEnginesStarting()) {
                AppUtils.sleep(500);
                if ((System.currentTimeMillis() - startTime) > maxWaitTimeInMs) {
                    throw new InterruptedException("Timed out waiting for engines to start");
                }
            }
        }
    }

    protected void setupBasicAuthIfNeeded(Server server) {
        if (StringUtils.isNotBlank(basicAuthUsername)) {
            ConstraintSecurityHandler sh = new ConstraintSecurityHandler();

            Constraint constraint = new Constraint();
            constraint.setName(Constraint.__BASIC_AUTH);

            constraint.setRoles(new String[] { SecurityConstants.EMBEDDED_WEBSERVER_DEFAULT_ROLE });
            constraint.setAuthenticate(true);

            ConstraintMapping cm = new ConstraintMapping();
            cm.setConstraint(constraint);
            cm.setPathSpec("/*");
            // sh.setConstraintMappings(new ConstraintMapping[] {cm});
            sh.addConstraintMapping(cm);

            sh.setAuthenticator(new BasicAuthenticator());

            HashLoginService loginService = new HashLoginService();
            loginService.putUser(basicAuthUsername, new Password(basicAuthPassword), null);
            sh.setLoginService(loginService);

            server.setHandler(sh);

        }
    }

    protected Connector[] getConnectors(Server server, int port, int securePort, Mode mode) {
        ArrayList<Connector> connectors = new ArrayList<Connector>();
        String keyStoreFile = System.getProperty(SecurityConstants.SYSPROP_KEYSTORE);
        String keyStoreType = System.getProperty(SecurityConstants.SYSPROP_KEYSTORE_TYPE,
                SecurityConstants.KEYSTORE_TYPE);

        HttpConfiguration httpConfig = new HttpConfiguration();
        if (mode.equals(Mode.HTTPS) || mode.equals(Mode.MIXED)) {
            httpConfig.setSecureScheme("https");
            httpConfig.setSecurePort(securePort);
        }

        httpConfig.setOutputBufferSize(32768);

        if (mode.equals(Mode.HTTP) || mode.equals(Mode.MIXED)) {
            ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
            http.setPort(port);
            http.setHost(host);
            http.setIdleTimeout(maxIdleTime);
            connectors.add(http);
            log.info(String.format("About to start %s web server on host:port %s:%s", name,
                    host == null ? "default" : host, port));
        }
        if (mode.equals(Mode.HTTPS) || mode.equals(Mode.MIXED)) {
            ISecurityService securityService = SecurityServiceFactory.create(SecurityServiceType.SERVER,
                    new TypedProperties(System.getProperties()));
            securityService.installDefaultSslCert(host);
            String keyStorePassword = System.getProperty(SecurityConstants.SYSPROP_KEYSTORE_PASSWORD);
            keyStorePassword = (keyStorePassword != null) ? keyStorePassword : SecurityConstants.KEYSTORE_PASSWORD;
            SslContextFactory sslConnectorFactory = new SslContextFactory();
            sslConnectorFactory.setKeyStorePath(keyStoreFile);
            sslConnectorFactory.setKeyManagerPassword(keyStorePassword);
            /* Prevent POODLE attack */
            sslConnectorFactory.addExcludeProtocols("SSLv3");
            sslConnectorFactory.setCertAlias(System.getProperty(SecurityConstants.SYSPROP_KEYSTORE_CERT_ALIAS,
                    SecurityConstants.ALIAS_SYM_PRIVATE_KEY));
            sslConnectorFactory.setKeyStoreType(keyStoreType);

            HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
            httpsConfig.addCustomizer(new SecureRequestCustomizer());

            ServerConnector https = new ServerConnector(server,
                    new SslConnectionFactory(sslConnectorFactory, HttpVersion.HTTP_1_1.asString()),
                    new HttpConnectionFactory(httpsConfig));
            https.setPort(securePort);
            https.setIdleTimeout(maxIdleTime);
            https.setHost(host);
            connectors.add(https);
            log.info(String.format("About to start %s web server on secure host:port %s:%s", name,
                    host == null ? "default" : host, securePort));
        }
        return connectors.toArray(new Connector[connectors.size()]);
    }

    protected void registerHttpJmxAdaptor(int jmxPort) throws Exception {
        if (AppUtils.isSystemPropertySet(SystemConstants.SYSPROP_JMX_HTTP_CONSOLE_ENABLED, true) && jmxEnabled) {
            log.info("Starting JMX HTTP console on port {}", jmxPort);
            MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
            ObjectName name = getHttpJmxAdaptorName();
            mbeanServer.createMBean(HttpAdaptor.class.getName(), name);
            if (!AppUtils.isSystemPropertySet(SystemConstants.SYSPROP_JMX_HTTP_CONSOLE_LOCALHOST_ENABLED, true)) {
                mbeanServer.setAttribute(name, new Attribute("Host", "0.0.0.0"));
            } else if (StringUtils.isNotBlank(host)) {
                mbeanServer.setAttribute(name, new Attribute("Host", host));
            }
            mbeanServer.setAttribute(name, new Attribute("Port", new Integer(jmxPort)));
            ObjectName processorName = getXslJmxAdaptorName();
            mbeanServer.createMBean(XSLTProcessor.class.getName(), processorName);
            mbeanServer.setAttribute(name, new Attribute("ProcessorName", processorName));
            mbeanServer.invoke(name, "start", null, null);
        }
    }

    protected ObjectName getHttpJmxAdaptorName() throws MalformedObjectNameException {
        return new ObjectName("Server:name=HttpAdaptor");
    }

    protected ObjectName getXslJmxAdaptorName() throws MalformedObjectNameException {
        return new ObjectName("Server:name=XSLTProcessor");
    }

    protected void removeHttpJmxAdaptor() {
        if (AppUtils.isSystemPropertySet(SystemConstants.SYSPROP_JMX_HTTP_CONSOLE_ENABLED, true) && jmxEnabled) {
            try {
                MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
                mbeanServer.unregisterMBean(getHttpJmxAdaptorName());
                mbeanServer.unregisterMBean(getXslJmxAdaptorName());
            } catch (Exception e) {
                log.warn("Could not unregister the JMX HTTP Adaptor");
            }
        }
    }

    public void stop() throws Exception {
        if (server != null) {
            removeHttpJmxAdaptor();
            server.stop();
        }
    }

    public static void main(String[] args) throws Exception {
        new SymmetricWebServer().start(8080, 8081);
    }

    public boolean isJoin() {
        return join;
    }

    public void setJoin(boolean join) {
        this.join = join;
    }

    public void setWebHome(String webHome) {
        this.webHome = webHome;
    }

    public int getMaxIdleTime() {
        return maxIdleTime;
    }

    public void setMaxIdleTime(int maxIdleTime) {
        this.maxIdleTime = maxIdleTime;
    }

    public void setHttpPort(int httpPort) {
        System.setProperty(ServerConstants.HTTP_PORT, Integer.toString(httpPort));
        this.httpPort = httpPort;
    }

    public int getHttpPort() {
        return httpPort;
    }

    public void setHttpsPort(int httpsPort) {
        System.setProperty(ServerConstants.HTTPS_PORT, Integer.toString(httpsPort));
        this.httpsPort = httpsPort;
    }

    public int getHttpsPort() {
        return httpsPort;
    }

    public void setPropertiesFile(String propertiesFile) {
        this.propertiesFile = propertiesFile;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public void setBasicAuthPassword(String basicAuthPassword) {
        this.basicAuthPassword = basicAuthPassword;
    }

    public void setBasicAuthUsername(String basicAuthUsername) {
        this.basicAuthUsername = basicAuthUsername;
    }

    public void setWebAppDir(String webAppDir) {
        this.webAppDir = webAppDir;
    }

    public void setNoNio(boolean noNio) {
        this.noNio = noNio;
    }

    public boolean isNoNio() {
        return noNio;
    }

    public void setNoDirectBuffer(boolean noDirectBuffer) {
        this.noDirectBuffer = noDirectBuffer;
    }

    public boolean isNoDirectBuffer() {
        return noDirectBuffer;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public int getJmxPort() {
        return jmxPort;
    }

    public void setJmxPort(int jmxPort) {
        this.jmxPort = jmxPort;
    }

    public void setHttpEnabled(boolean httpEnabled) {
        this.httpEnabled = httpEnabled;
    }

    public boolean isHttpEnabled() {
        return httpEnabled;
    }

    public void setHttpsEnabled(boolean httpsEnabled) {
        this.httpsEnabled = httpsEnabled;
    }

    public boolean isHttpsEnabled() {
        return httpsEnabled;
    }

    public void setJmxEnabled(boolean jmxEnabled) {
        this.jmxEnabled = jmxEnabled;
    }

    public boolean isJmxEnabled() {
        return jmxEnabled;
    }

    class SessionManager extends HashSessionManager {

        public SessionManager() {
            setMaxInactiveInterval(10 * 60);
            setLazyLoad(true);
            setDeleteUnrestorableSessions(true);
            setSessionCookie(getSessionCookie() + (httpPort > 0 ? httpPort : httpsPort));
        }

        @Override
        protected AbstractSession newSession(HttpServletRequest request) {
            return new Session(this, request);
        }

        @Override
        protected AbstractSession newSession(long created, long accessed, String clusterId) {
            return new Session(this, created, accessed, clusterId);
        }

        @Override
        protected synchronized HashedSession restoreSession(String idInCuster) {
            if (isNotBlank(idInCuster)) {
                return super.restoreSession(idInCuster);
            } else {
                return null;
            }
        }

        public HashedSession restoreSession(InputStream is, HashedSession session) throws Exception {
            DataInputStream di = new DataInputStream(is);

            String clusterId = di.readUTF();
            di.readUTF(); // nodeId

            long created = di.readLong();
            long accessed = di.readLong();
            int requests = di.readInt();

            if (session == null)
                session = (HashedSession) newSession(created, accessed, clusterId);
            session.setRequests(requests);

            int size = di.readInt();

            restoreSessionAttributes(di, size, session);

            try {
                int maxIdle = di.readInt();
                session.setMaxInactiveInterval(maxIdle);
            } catch (EOFException e) {
                log.debug("No maxInactiveInterval persisted for session " + clusterId, e);
            }

            return session;
        }

        private void restoreSessionAttributes(InputStream is, int size, HashedSession session) throws Exception {
            if (size > 0) {
                ObjectInputStream ois = new ObjectInputStream(is);
                for (int i = 0; i < size; i++) {
                    String key = ois.readUTF();
                    try {
                        Object value = ois.readObject();
                        session.setAttribute(key, value);
                    } catch (Exception ex) {
                        if (ex instanceof ClassCastException || ex instanceof ClassNotFoundException) {
                            log.warn("Could not restore the '" + key
                                    + "' session object.  Code has probably changed.  The error message was: "
                                    + ex.getMessage());
                        } else {
                            log.error("Could not restore the '" + key + "' session object.", ex);
                        }
                    }
                }
            }
        }

    }

    class Session extends HashedSession {

        protected Session(HashSessionManager hashSessionManager, HttpServletRequest request) {
            super(hashSessionManager, request);
        }

        protected Session(HashSessionManager hashSessionManager, long created, long accessed, String clusterId) {
            super(hashSessionManager, created, accessed, clusterId);
        }

        @Override
        public synchronized void save(OutputStream os) throws IOException {
            DataOutputStream out = new DataOutputStream(os);
            out.writeUTF(getClusterId());
            out.writeUTF(getNodeId());
            out.writeLong(getCreationTime());
            out.writeLong(getAccessed());
            out.writeInt(getRequests());

            Enumeration<String> e = getAttributeNames();
            int count = 0;
            while (e.hasMoreElements()) {
                String key = e.nextElement();
                Object obj = doGet(key);
                if (obj instanceof Serializable) {
                    count++;
                }
            }
            out.writeInt(count);
            ObjectOutputStream oos = new ObjectOutputStream(out);
            e = getAttributeNames();
            while (e.hasMoreElements()) {
                String key = e.nextElement();
                Object obj = doGet(key);
                if (obj instanceof Serializable) {
                    oos.writeUTF(key);
                    oos.writeObject(obj);
                }
            }
            oos.flush();
            out.writeInt(getMaxInactiveInterval());
        }

    }

}