ca.sqlpower.wabit.enterprise.client.WabitClientSession.java Source code

Java tutorial

Introduction

Here is the source code for ca.sqlpower.wabit.enterprise.client.WabitClientSession.java

Source

/*
 * Copyright (c) 2009, SQL Power Group Inc.
 *
 * This file is part of Wabit.
 *
 * Wabit 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.
 *
 * Wabit 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 <http://www.gnu.org/licenses/>. 
 */

package ca.sqlpower.wabit.enterprise.client;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.annotation.Nonnull;
import javax.swing.SwingUtilities;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import ca.sqlpower.dao.MessageSender;
import ca.sqlpower.dao.SPPersistenceException;
import ca.sqlpower.dao.json.JSONHttpMessageSender;
import ca.sqlpower.dao.json.SPJSONMessageDecoder;
import ca.sqlpower.enterprise.client.SPServerInfo;
import ca.sqlpower.http.HttpResponseHandler;
import ca.sqlpower.sql.DataSourceCollection;
import ca.sqlpower.sql.DatabaseListChangeEvent;
import ca.sqlpower.sql.DatabaseListChangeListener;
import ca.sqlpower.sql.JDBCDataSource;
import ca.sqlpower.sql.Olap4jDataSource;
import ca.sqlpower.sql.PlDotIni;
import ca.sqlpower.sql.SPDataSource;
import ca.sqlpower.util.UserPrompter.UserPromptOptions;
import ca.sqlpower.util.UserPrompter.UserPromptResponse;
import ca.sqlpower.util.UserPrompterFactory.UserPromptType;
import ca.sqlpower.wabit.WabitSession;
import ca.sqlpower.wabit.WabitSessionContext;
import ca.sqlpower.wabit.WabitSessionImpl;
import ca.sqlpower.wabit.WabitWorkspace;
import ca.sqlpower.wabit.dao.WabitSessionPersister;
import ca.sqlpower.wabit.dao.json.WabitJSONPersister;
import ca.sqlpower.wabit.dao.session.WorkspacePersisterListener;
import ca.sqlpower.wabit.swingui.WabitSwingSessionContext;

/**
 * A special kind of session that binds itself to a remote Wabit Enterprise
 * Server. Provides database connection information and file storage capability
 * based on the remote server.
 */
public class WabitClientSession extends WabitSessionImpl {

    private static final Logger logger = Logger.getLogger(WabitClientSession.class);

    /**
     * The relative path to the Mondrian schemas from the server's base URI.
     */
    private static final String MONDRIAN_SCHEMA_REL_PATH = "mondrian-schema/";

    private final Updater updater;

    /**
     * This workspace's location information.
     */
    private final WorkspaceLocation workspaceLocation;

    private final HttpClient outboundHttpClient;

    /**
     * Handles output Wabit persistence calls for this WabitServerSession
     */
    private final WabitJSONPersister jsonPersister;

    /**
     * Applies Wabit persistence calls coming from a Wabit server to this WabitServerSession
     */
    private final WabitSessionPersister sessionPersister;

    private static CookieStore cookieStore = new BasicCookieStore();

    /**
     * The data source collection retrieved from the server. This field is
     * lazy-loaded; it should always be accessed by calling
     * {@link #getDataSources()}. This data source is monitored for changes, and
     * those changes are posted back to the server.
     */
    private DataSourceCollection<SPDataSource> dataSourceCollection;

    /**
     * Sends changes to the local copy of the data source collection back to the
     * server. Gets detached and shut down when this session is closed.
     */
    private final DataSourceCollectionUpdater dataSourceCollectionUpdater = new DataSourceCollectionUpdater();

    public WabitClientSession(@Nonnull WorkspaceLocation workspaceLocation, @Nonnull WabitSessionContext context) {
        super(context);
        this.workspaceLocation = workspaceLocation;
        if (workspaceLocation == null) {
            throw new NullPointerException("workspaceLocation must not be null");
        }

        super.fontLoader = new RemoteFontLoader(workspaceLocation.getServiceInfo());
        outboundHttpClient = createHttpClient(workspaceLocation.getServiceInfo());

        getWorkspace().setUUID(workspaceLocation.getUuid());
        getWorkspace().setName("Loading Workspace...");
        getWorkspace().setSession(this); // XXX leaking a reference to partially-constructed session!

        sessionPersister = new WabitSessionPersister("inbound-" + workspaceLocation.getUuid(),
                WabitClientSession.this, true);
        // Whatever updates come from the server, it can override the user's stuff.
        sessionPersister.setGodMode(true);
        updater = new Updater(workspaceLocation.getUuid(), new SPJSONMessageDecoder(sessionPersister));

        MessageSender<JSONObject> httpSender = new JSONHttpMessageSender(outboundHttpClient,
                workspaceLocation.getServiceInfo(), workspaceLocation.getUuid());
        jsonPersister = new WabitJSONPersister(httpSender);

        try {
            ServerInfoProvider.getServerVersion(this.workspaceLocation.getServiceInfo().getServerAddress(),
                    String.valueOf(this.workspaceLocation.getServiceInfo().getPort()),
                    this.workspaceLocation.getServiceInfo().getPath(),
                    this.workspaceLocation.getServiceInfo().getUsername(),
                    this.workspaceLocation.getServiceInfo().getPassword());
        } catch (Exception e) {
            throw new AssertionError("Exception encountered while verifying the server license:" + e.getMessage());
        }
    }

    public static HttpClient createHttpClient(SPServerInfo serviceInfo) {
        HttpParams params = new BasicHttpParams();
        HttpConnectionParams.setConnectionTimeout(params, 2000);
        DefaultHttpClient httpClient = new DefaultHttpClient(params);
        httpClient.setCookieStore(cookieStore);
        httpClient.getCredentialsProvider().setCredentials(
                new AuthScope(serviceInfo.getServerAddress(), AuthScope.ANY_PORT),
                new UsernamePasswordCredentials(serviceInfo.getUsername(), serviceInfo.getPassword()));
        return httpClient;
    }

    @Override
    public boolean close() {
        logger.debug("Closing Client Session");
        try {
            HttpUriRequest request = new HttpDelete(
                    getServerURI(workspaceLocation.getServiceInfo(), "session/" + getWorkspace().getUUID()));
            outboundHttpClient.execute(request, new BasicResponseHandler());
        } catch (Exception e) {
            try {
                logger.error(e);
                getContext().createUserPrompter("Cannot access the server to close the server session",
                        UserPromptType.MESSAGE, UserPromptOptions.OK, UserPromptResponse.OK, UserPromptResponse.OK,
                        "OK");
            } catch (Throwable t) {
                //do nothing here because we failed on logging the error.
            }
        }
        outboundHttpClient.getConnectionManager().shutdown();
        updater.interrupt();

        if (dataSourceCollection != null) {
            dataSourceCollectionUpdater.detach(dataSourceCollection);
        }

        return super.close();
    }

    /**
     * Returns the location this workspace was loaded from.
     */
    public WorkspaceLocation getWorkspaceLocation() {
        return workspaceLocation;
    }

    /**
     * Sends local data source collection changes to the server. In order for
     * this to work, the data source collection must be attached. To prevent
     * memory leaks, the collection updater must be detached from all data
     * source collections it was monitoring when the Wabit session is closed.
     */
    private class DataSourceCollectionUpdater implements DatabaseListChangeListener, PropertyChangeListener {

        /**
         * If true this updater is currently posting properties to the server. If
         * properties are being posted to the server and an event comes in because
         * of a change during posting the updater should not try to repost the message
         * it is currently trying to post.
         */
        private boolean postingProperties = false;

        public void attach(DataSourceCollection<SPDataSource> dsCollection) {
            dsCollection.addDatabaseListChangeListener(this);
            for (SPDataSource ds : dsCollection.getConnections()) {
                ds.addPropertyChangeListener(this);
            }
        }

        public void detach(DataSourceCollection<SPDataSource> dsCollection) {
            dsCollection.removeDatabaseListChangeListener(this);
            for (SPDataSource ds : dsCollection.getConnections()) {
                ds.removePropertyChangeListener(this);
            }
        }

        /**
         * Handles the addition of a new database entry, relaying its current
         * state to the server. Also begins listening to the new data source as
         * would have happened if the new data source existed before
         * {@link #attach(DataSourceCollection)} was invoked.
         */
        public void databaseAdded(DatabaseListChangeEvent e) {
            SPDataSource newDS = e.getDataSource();
            newDS.addPropertyChangeListener(this);

            List<NameValuePair> properties = new ArrayList<NameValuePair>();
            for (Map.Entry<String, String> ent : newDS.getPropertiesMap().entrySet()) {
                properties.add(new BasicNameValuePair(ent.getKey(), ent.getValue()));
            }

            postPropertiesToServer(newDS, properties);
        }

        /**
         * Handles changes to individual data sources by relaying their new
         * state to the server.
         * <p>
         * <b>Implementation note:</b> Presently, all properties for the data
         * source are sent back to the server every time one of them changes.
         * This is not the desired behaviour, but without rethinking the
         * SPDataSource event system, there is little else we can do: the
         * property change events tell us JavaBeans property names, but in order
         * to send incremental updates, we's need to know the pl.ini property
         * key names.
         * 
         * @param evt
         *            The event describing the change. Its source must be the
         *            data source object which was modified.
         */
        public void propertyChange(PropertyChangeEvent evt) {
            SPDataSource ds = (SPDataSource) evt.getSource();
            ds.addPropertyChangeListener(this);

            // Updating all properties is less than ideal, but a property change event does
            // not tell us what the "pl.ini" key for the property is.
            List<NameValuePair> properties = new ArrayList<NameValuePair>();
            for (Map.Entry<String, String> ent : ds.getPropertiesMap().entrySet()) {
                properties.add(new BasicNameValuePair(ent.getKey(), ent.getValue()));
            }

            postPropertiesToServer(ds, properties);
        }

        /**
         * Modifies the properties of the given data source on the server. If
         * the given data source does not exist on the server, it will be
         * created with all of the given properties.
         * 
         * @param ds
         *            The data source to update on the server.
         * @param properties
         *            The properties to update. No properties will be removed
         *            from the server, and only the given properties will be
         *            updated or created.
         */
        private void postPropertiesToServer(SPDataSource ds, List<NameValuePair> properties) {
            if (postingProperties)
                return;

            HttpClient httpClient = createHttpClient(workspaceLocation.getServiceInfo());
            try {
                final ResponseHandler<Void> responseHandler = new ResponseHandler<Void>() {
                    public Void handleResponse(HttpResponse response) throws ClientProtocolException, IOException {

                        if (response.getStatusLine().getStatusCode() != 200) {
                            throw new ClientProtocolException(
                                    "Failed to create/update data source on server. Reason:\n"
                                            + EntityUtils.toString(response.getEntity()));
                        } else {
                            // success!
                            return null;
                        }

                    }
                };

                if (ds instanceof Olap4jDataSource && ((Olap4jDataSource) ds).getMondrianSchema() != null
                        && ((Olap4jDataSource) ds).getMondrianSchema().getScheme().equals("file")) {
                    //Pushing the mondrian schema to the server and updating the schema location to a server schema
                    Olap4jDataSource olapDS = ((Olap4jDataSource) ds);
                    File schemaFile = new File(olapDS.getMondrianSchema());

                    if (!schemaFile.exists())
                        logger.error("Schema file " + schemaFile.getAbsolutePath()
                                + " does not exist for data source " + ds.getName());

                    HttpPost request = new HttpPost(getServerURI(workspaceLocation.getServiceInfo(),
                            MONDRIAN_SCHEMA_REL_PATH + schemaFile.getName()));

                    request.setEntity(new FileEntity(schemaFile, "text/xml"));
                    httpClient.execute(request, responseHandler);

                    //updating new data source to point to the server's schema.
                    for (int i = properties.size() - 1; i >= 0; i--) {
                        NameValuePair pair = properties.get(i);
                        if (pair.getName().equals(Olap4jDataSource.MONDRIAN_SCHEMA)) {
                            properties.add(new BasicNameValuePair(Olap4jDataSource.MONDRIAN_SCHEMA,
                                    SPDataSource.SERVER + schemaFile.getName()));
                            properties.remove(pair);
                            break;
                        }
                    }

                    try {
                        postingProperties = true;
                        olapDS.setMondrianSchema(new URI(SPDataSource.SERVER + schemaFile.getName()));
                    } finally {
                        postingProperties = false;
                    }
                }

                HttpPost request = new HttpPost(dataSourceURI(ds));

                request.setEntity(new UrlEncodedFormEntity(properties));
                httpClient.execute(request, responseHandler);

            } catch (Exception ex) {
                throw new RuntimeException(ex);
            } finally {
                httpClient.getConnectionManager().shutdown();
            }
        }

        /**
         * Handles deleting of a database entry by requesting that the server
         * deletes it. Also unlistens to the data source to prevent memory
         * leaks.
         */
        public void databaseRemoved(DatabaseListChangeEvent e) {
            HttpClient httpClient = createHttpClient(workspaceLocation.getServiceInfo());
            try {
                SPDataSource removedDS = e.getDataSource();

                HttpDelete request = new HttpDelete(dataSourceURI(removedDS));

                final ResponseHandler<Void> responseHandler = new ResponseHandler<Void>() {
                    public Void handleResponse(HttpResponse response) throws ClientProtocolException, IOException {

                        if (response.getStatusLine().getStatusCode() != 200) {
                            throw new ClientProtocolException("Failed to delete data source on server. Reason:\n"
                                    + EntityUtils.toString(response.getEntity()));
                        } else {
                            // success!
                            return null;
                        }

                    }
                };
                httpClient.execute(request, responseHandler);

                if (removedDS instanceof Olap4jDataSource
                        && ((Olap4jDataSource) removedDS).getMondrianSchema() != null) {
                    URI serverURI = ((Olap4jDataSource) removedDS).getMondrianSchema();
                    logger.debug("Server URI for deletion is " + serverURI);
                    HttpDelete schemaRequest = new HttpDelete(serverURI);
                    httpClient.execute(schemaRequest, responseHandler);
                }

            } catch (Exception ex) {
                throw new RuntimeException(ex);
            } finally {
                httpClient.getConnectionManager().shutdown();
            }
        }

        /**
         * Returns the URI that references the given data source on the server.
         * 
         * @param ds
         *            The data source whose server URI to return.
         * @return An absolute URI for the given data source on this session's
         *         Wabit server.
         */
        private URI dataSourceURI(SPDataSource ds) throws URISyntaxException {
            String type;
            if (ds instanceof JDBCDataSource) {
                type = "jdbc";
            } else if (ds instanceof Olap4jDataSource) {
                type = "olap4j";
            } else {
                throw new UnsupportedOperationException("Data source type " + ds.getClass() + " is not known");
            }

            return getServerURI(workspaceLocation.getServiceInfo(), "data-sources/" + type + "/" + ds.getName());
        }
    }

    /**
     * Returns the server's data source list, retrieving it from the server if
     * that has not already been done during this session. Changes made to this
     * data source collection will be sent back to the server, but the changes
     * will not be applied on the server side unless the user has the
     * appropriate permissions.
     * <p>
     * Future plans: In the future, the server will probably be a proxy for all
     * database operations, and we won't actually send the connection
     * information to the client. This has the advantage that it can work over
     * an HTTP firewall or proxy, where the present method would fail.
     */
    @Override
    public DataSourceCollection<SPDataSource> getDataSources() {

        if (dataSourceCollection != null)
            return dataSourceCollection;

        ResponseHandler<DataSourceCollection<SPDataSource>> plIniHandler = new ResponseHandler<DataSourceCollection<SPDataSource>>() {
            public DataSourceCollection<SPDataSource> handleResponse(HttpResponse response)
                    throws ClientProtocolException, IOException {
                if (response.getStatusLine().getStatusCode() != 200) {
                    throw new IOException("Server error while reading data sources: " + response.getStatusLine());
                }
                PlDotIni plIni;
                try {
                    plIni = new PlDotIni(getServerURI(workspaceLocation.getServiceInfo(), "jdbc/"),
                            getServerURI(workspaceLocation.getServiceInfo(), MONDRIAN_SCHEMA_REL_PATH));
                    plIni.read(response.getEntity().getContent());
                    logger.debug("Data source collection has URI " + plIni.getServerBaseURI());
                } catch (URISyntaxException e) {
                    throw new RuntimeException(e);
                }
                return plIni;
            }
        };
        try {
            dataSourceCollection = executeServerRequest(outboundHttpClient, workspaceLocation.getServiceInfo(),
                    "data-sources/", plIniHandler);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }

        dataSourceCollectionUpdater.attach(dataSourceCollection);

        return dataSourceCollection;
    }

    /**
     * List all the workspaces on this context's server.
     * 
     * @param serviceInfo
     * @return
     * @throws IOException
     * @throws URISyntaxException
     * @throws JSONException 
     */
    public static List<WorkspaceLocation> getWorkspaceNames(SPServerInfo serviceInfo)
            throws IOException, URISyntaxException, JSONException {
        HttpClient httpClient = createHttpClient(serviceInfo);
        try {
            HttpUriRequest request = new HttpGet(getServerURI(serviceInfo, "workspaces"));
            String responseBody = httpClient.execute(request, new BasicResponseHandler());
            JSONArray response;
            List<WorkspaceLocation> workspaces = new ArrayList<WorkspaceLocation>();
            response = new JSONArray(responseBody);
            logger.debug("Workspace list:\n" + responseBody);
            for (int i = 0; i < response.length(); i++) {
                JSONObject workspace = (JSONObject) response.get(i);
                workspaces.add(new WorkspaceLocation(workspace.getString("name"), workspace.getString("UUID"),
                        serviceInfo));
            }
            return workspaces;
        } finally {
            httpClient.getConnectionManager().shutdown();
        }
    }

    /**
     * Sends an HTTP request to a Wabit Enterprise Server to create a new remote
     * Wabit Workspace on that server.
     * 
     * @param serviceInfo
     *            A {@link SPServerInfo} containing the connection
     *            information for that server
     * @return The {@link WorkspaceLocation} of the newly created remote
     *         WabitWorkspace
     * @throws URISyntaxException
     * @throws ClientProtocolException
     * @throws IOException
     * @throws JSONException
     */
    public static WorkspaceLocation createNewServerSession(SPServerInfo serviceInfo)
            throws URISyntaxException, ClientProtocolException, IOException, JSONException {
        HttpClient httpClient = createHttpClient(serviceInfo);
        try {
            HttpUriRequest request = new HttpPost(getServerURI(serviceInfo, "workspaces"));
            String responseBody = httpClient.execute(request, new BasicResponseHandler());
            JSONObject response = new JSONObject(responseBody);
            logger.debug("New Workspace:" + responseBody);
            return new WorkspaceLocation(response.getString("name"), response.getString("UUID"), serviceInfo);
        } finally {
            httpClient.getConnectionManager().shutdown();
        }
    }

    public void deleteServerWorkspace() throws URISyntaxException, ClientProtocolException, IOException {
        SPServerInfo serviceInfo = workspaceLocation.getServiceInfo();
        HttpClient httpClient = createHttpClient(serviceInfo);
        try {
            HttpUriRequest request = new HttpDelete(
                    getServerURI(serviceInfo, "workspaces/" + getWorkspace().getUUID()));
            httpClient.execute(request, new HttpResponseHandler());
        } finally {
            httpClient.getConnectionManager().shutdown();
        }
    }

    /**
     * Finds and opens a specific Wabit Workspace from the given
     * {@link WorkspaceLocation}. The new session will keep itself up-to-date by
     * polling the server for new state. Likewise, local changes to the session will be pushed its own
     * updates back to the server.
     * 
     * @param context
     *            The context to register the new remote WabitSession with
     * @param workspaceLoc
     *            A {@link WorkspaceLocation} detailing the location of the
     *            remote workspace to be opened
     * @return A remote WabitSession based on the given workspace
     */
    public static WabitClientSession openServerSession(WabitSessionContext context,
            WorkspaceLocation workspaceLoc) {
        final WabitClientSession session = new WabitClientSession(workspaceLoc, context);
        context.registerChildSession(session);
        session.startUpdaterThread();
        return session;
    }

    /**
     * Finds and opens all visible Wabit workspaces on the given Wabit Enterprise Server.
     * Calling this method essentially constitutes "logging in" to the given server.
     * 
     * @param context the context to add the newly-retrieved sessions to
     * @param serverInfo The server to contact.
     * @return the list of sessions that were opened.
     * @throws JSONException 
     * @throws URISyntaxException 
     * @throws IOException 
     */
    public static List<WabitClientSession> openServerSessions(WabitSessionContext context, SPServerInfo serverInfo)
            throws IOException, URISyntaxException, JSONException {
        List<WabitClientSession> openedSessions = new ArrayList<WabitClientSession>();
        for (WorkspaceLocation workspaceLoc : WabitClientSession.getWorkspaceNames(serverInfo)) {
            openedSessions.add(openServerSession(context, workspaceLoc));
        }
        return openedSessions;
    }

    private static <T> T executeServerRequest(HttpClient httpClient, SPServerInfo serviceInfo,
            String contextRelativePath, ResponseHandler<T> responseHandler) throws IOException, URISyntaxException {
        HttpUriRequest request = new HttpGet(getServerURI(serviceInfo, contextRelativePath));
        return httpClient.execute(request, responseHandler);
    }

    private static URI getServerURI(SPServerInfo serviceInfo, String contextRelativePath)
            throws URISyntaxException {
        logger.debug("Getting server URI for: " + serviceInfo);
        String contextPath = serviceInfo.getPath();
        URI serverURI = new URI("http", null, serviceInfo.getServerAddress(), serviceInfo.getPort(),
                contextPath + contextRelativePath, null, null);
        logger.debug("Created URI " + serverURI);
        return serverURI;
    }

    public void startUpdaterThread() {
        updater.start();
        WorkspacePersisterListener.attachListener(this, jsonPersister, sessionPersister, true);
    }

    public void persistWorkspaceToServer() throws SPPersistenceException {
        WorkspacePersisterListener tempListener = new WorkspacePersisterListener(this, jsonPersister, true);
        tempListener.persistObject(this.getWorkspace());
    }

    /**
     * Polls this session's server for updates until interrupted. There should
     * be exactly one instance of this class per WabitServerSession.
     */
    private class Updater extends Thread {

        /**
         * How long we will pause after an update error before attempting to
         * contact the server again.
         */
        private long retryDelay = 1000;

        private final SPJSONMessageDecoder jsonDecoder;

        /**
         * Used by the Updater to handle inbound HTTP updates
         */
        private final HttpClient inboundHttpClient;

        private volatile boolean cancelled;

        /**
         * Creates, but does not start, the updater thread.
         * 
         * @param workspaceUUID
         *            the ID of the workspace this updater is responsible for. This is
         *            used in creating the thread's name.
         */
        Updater(String workspaceUUID, SPJSONMessageDecoder jsonDecoder) {
            super("updater-" + workspaceUUID);
            this.jsonDecoder = jsonDecoder;
            inboundHttpClient = createHttpClient(workspaceLocation.getServiceInfo());
        }

        public void interrupt() {
            logger.debug("Updater Thread interrupt sent");
            super.interrupt();
            cancelled = true;
        }

        @Override
        public void run() {
            logger.info("Updater thread starting");

            // the path to contact on the server for update events
            final String contextRelativePath = "workspaces/" + getWorkspace().getUUID();

            try {
                while (!this.isInterrupted() && !cancelled) {
                    try {
                        final String jsonArray = executeServerRequest(inboundHttpClient,
                                workspaceLocation.getServiceInfo(), contextRelativePath,
                                new BasicResponseHandler());
                        runInForeground(new Runnable() {
                            public void run() {
                                try {
                                    jsonDecoder.decode(jsonArray);
                                } catch (SPPersistenceException e) {
                                    logger.error("Update from server failed!", e);
                                    createUserPrompter(
                                            "Wabit failed to apply an update that was just received from the Enterprise Server.\n"
                                                    + "The error was:" + "\n" + e.getMessage(),
                                            UserPromptType.MESSAGE, UserPromptOptions.OK, UserPromptResponse.OK,
                                            UserPromptResponse.OK, "OK");
                                    // TODO discard session and reload
                                }
                            }
                        });
                    } catch (Exception ex) {
                        logger.error("Failed to contact server. Will retry in " + retryDelay + " ms.", ex);
                        Thread.sleep(retryDelay);
                    }
                }
            } catch (InterruptedException ex) {
                logger.info("Updater thread exiting normally due to interruption.");
            }

            inboundHttpClient.getConnectionManager().shutdown();
        }
    }

    /**
     * Fetches the system workspace from the same server as this session.
     * Returns null if the user doesn't have access to a given workspace.
     */
    public WabitWorkspace getSystemWorkspace() {
        for (WabitSession session : this.getContext().getSessions()) {
            if (session.getWorkspace().getUUID().equals("system")) {
                return session.getWorkspace();
            }
        }
        return null;
    }

    @Override
    public void runInForeground(Runnable runner) {
        // If we're in a SwingContext, run on the Swing Event Dispatch thread.
        // XXX: This is a bit of a quickfix and I think a better way to possibly fix
        // this could be to have WabitServerSession implement WabitSession, and
        // use a delegate session to delegate most of the server calls (instead
        // of extending WabitSessionImpl). Then if it's in a swing context, it would
        // have a WabitSwingSession instead.
        if (getContext() instanceof WabitSwingSessionContext) {
            SwingUtilities.invokeLater(runner);
        } else {
            super.runInForeground(runner);
        }
    }

    @Override
    public boolean isEnterpriseServerSession() {
        return true;
    }

    /**
     * Exposes the shared cookie store so we don't spawn useless sessions
     * through the client.
     */
    public static CookieStore getCookieStore() {
        return cookieStore;
    }

}