org.chromium.sdk/src/org/chromium/sdk/internal/SessionManager.java
changeset 2 e4420d2515f1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/org.chromium.sdk/src/org/chromium/sdk/internal/SessionManager.java	Wed Dec 23 17:13:18 2009 -0800
@@ -0,0 +1,242 @@
+// Copyright (c) 2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.sdk.internal;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Manager that switches on and off some resource for a shared multiuser access.
+ * The nature of actual resource should be defined in subclass of
+ * {@link SessionManager}. Time period when resource is on is called "session".
+ * Switch on operation (aka session creation) must be an atomic operation.
+ * Switch off (aka session closing) may be lengthy asynchronous operation.
+ * <p>
+ * If no user needs it, manager switches the resource off. On the first demand
+ * resource gets switched on (and new session gets created). After the last user
+ * has released the resource, the session finishes either instantly or
+ * some time later. In the latter case resource becomes temporary unavailable.
+ * The manager does not operate resource in any other sense than switching it
+ * on and off.
+ * <p>
+ * Every user first acquires the resource by calling {@link #connect()} method.
+ * It gets ticket which points to the corresponding session. Method
+ * {@link Ticket#dismiss()} must be called when resource is no more needed.
+ * @param <SESSION> user class that represents a session; must
+ *                  extend {@link SessionBase}
+ * @param <EX> exception that is allowed to be thrown when resource is being switched
+ *             on and the new session is starting; {@link RuntimeException}
+ *             is a good default parameter value
+ */
+public abstract class SessionManager<SESSION extends SessionManager.SessionBase<SESSION>,
+    EX extends Exception> {
+
+  // Holds current session; all access must be synchronized on "this".
+  private SESSION currentSession = null;
+
+  /**
+   * Ticket to resource use. Every client gets its own copy. All tickets must
+   * be dismissed in order for resource to be switched off.
+   * @param <SESSION> is be the same type as of manager that issued this ticket
+   */
+  public interface Ticket<SESSION> {
+    /**
+     * Each valid ticket points to session of the resource. The actual type
+     * {@code SESSION} is provided by user (as a type parameter of enclosing
+     * SessionManager). The actual resource should be accessible from
+     * {@code SESSION}.
+     * @return non-null current session
+     * @throws IllegalStateException if ticket is no longer valid
+     */
+    SESSION getSession();
+
+    /**
+     * Releases resource and makes ticket invalid. Switches the resource
+     * off if it was a last ticket.
+     * @throws IllegalStateException if ticket is no more valid
+     */
+    void dismiss();
+  }
+
+  /**
+   * Registers user request for resource and switches the resource on if required.
+   * @return new ticket which symbolize use of resource until
+   *             {@link Ticket#dismiss()} is called
+   * @throws EX if connect required creating a new session and if the new session creation
+   *         has failed
+   */
+  public Ticket<SESSION> connect() throws EX {
+    synchronized (this) {
+      if (currentSession != null) {
+        // this may reset currentSession
+        currentSession.checkHealth();
+      }
+      if (currentSession == null) {
+        currentSession = newSessionObject();
+        if (currentSession.manager != this) {
+          throw new IllegalArgumentException("Wrong manager was set in session");
+        }
+      }
+      return currentSession.newTicket();
+    }
+  }
+
+  /**
+   * User-provided constructor of a new session. It should switch the resource on
+   * whatever it actually means.
+   * @return new instance of resource use session
+   * @throws EX if switching resource on or creating a new session failed
+   */
+  protected abstract SESSION newSessionObject() throws EX;
+
+  /**
+   * Base class for user session. It should be subclassed and it is parameterized by
+   * this subclass. Object construction should have semantics of switching resource
+   * on. It gets constructed via user-defined {@link SessionManager#newSessionObject()}.
+   * Subclass should honestly pass instance of {@link SessionManager} to the base
+   * class. User also should implement {@link #lastTicketDismissed()} and helper
+   * {@link #getThisAsSession()}.
+   * @param <SESSION> the very user class which extends {@link SessionBase};
+   *                  {@link #getThisAsSession()} should compile as "return this;"
+   */
+  public static abstract class SessionBase<SESSION extends SessionBase<SESSION>> {
+    private final SessionManager<?, ?> manager;
+    private boolean isConnectionStopped = false;
+    private boolean isCancelled = false;
+
+    SessionBase(SessionManager<SESSION, ?> manager) {
+      this.manager = manager;
+    }
+
+    /**
+     * Must be simply "return this;"
+     */
+    protected abstract SESSION getThisAsSession();
+
+    /**
+     * Session may check its health here. This check is made on
+     * every new connection. If it appears that the session is no longer alive
+     * the method should call {@link #interruptSession()}. However, this is a highly
+     * unwanted scenario: session should interrupt itself synchronously, no
+     * on-demand from this method.
+     */
+    protected abstract void checkHealth();
+
+    /**
+     * User-provided behavior when no more valid tickets left. Resource should
+     * be switched off whatever it actually means and the session closed.
+     * There are 3 options here:
+     * <ol>
+     * <li>Method is finished with {@link #closeSession()} call. Method
+     * {@link SessionManager#connect()} does not interrupt its service and simply
+     * creates new session the next call.
+     * <li>Method is finished with {@link #stopNewConnections()} call. Connection
+     * process is put on hold after this and {@link SessionManager#connect()} starts
+     * to throw {@link IllegalStateException}. Later {@link #closeSession()} must
+     * be called possibly asynchronously. After this the resource is available again
+     * and a new session may be created.
+     * <li>Do not call any of methods listed above. This probably works but is
+     * not specified here.
+     * </ol>
+     */
+    protected abstract void lastTicketDismissed();
+
+    /**
+     * See {@link #lastTicketDismissed()}. This method is supposed to be called
+     * from there, but not necessarily.
+     */
+    protected void stopNewConnections() {
+      synchronized (manager) {
+        isConnectionStopped = true;
+      }
+    }
+
+    /**
+     * Stops all new connections and cancels all existing tickets. Don't forget
+     * to call {@link #closeSession()} manually.
+     * @return collection of exceptions we gathered from tickets
+     */
+    protected Collection<? extends RuntimeException> interruptSession() {
+      synchronized (manager) {
+        isConnectionStopped = true;
+        isCancelled = true;
+        // TODO(peter.rybin): notify listeners here in case they are interested
+        tickets.clear();
+      }
+
+      return Collections.emptyList();
+    }
+
+    /**
+     * See {@link #lastTicketDismissed()}. This method is supposed to be called
+     * from there, but not necessarily.
+     */
+    protected void closeSession() {
+      synchronized (manager) {
+        isConnectionStopped = true;
+        if (!tickets.isEmpty()) {
+          throw new IllegalStateException("Some tickets are still valid");
+        }
+        if (manager.currentSession != this) {
+          throw new IllegalStateException("Session is not active");
+        }
+        manager.currentSession = null;
+      }
+    }
+
+    /**
+     * Creates new ticket that is to be dismissed later.
+     * Internal method. However user may use it or even make it public.
+     */
+    protected Ticket<SESSION> newTicket() {
+      synchronized (manager) {
+        if (isConnectionStopped) {
+          throw new IllegalStateException("Connection has been stopped");
+        }
+        TicketImpl ticketImpl = new TicketImpl();
+        tickets.add(ticketImpl);
+        return ticketImpl;
+      }
+    }
+
+    private final List<TicketImpl> tickets = new ArrayList<TicketImpl>();
+
+    private class TicketImpl implements Ticket<SESSION> {
+      private volatile boolean isDismissed = false;
+
+      public void dismiss() {
+        synchronized (manager) {
+          if (!isCancelled) {
+            boolean res = tickets.remove(this);
+            if (!res) {
+              throw new IllegalStateException("Ticket is already dismissed");
+            }
+            if (tickets.isEmpty()) {
+              lastTicketDismissed();
+            }
+          }
+          isDismissed = true;
+        }
+      }
+
+      public SESSION getSession() {
+        if (isDismissed) {
+          throw new IllegalStateException("Ticket is dismissed");
+        }
+        return getThisAsSession();
+      }
+    }
+  }
+
+  /**
+   * This method is completely unsynchronized. Is should be used for
+   * single-threaded tests only.
+   */
+  public SESSION getCurrentSessionForTest() {
+    return currentSession;
+  }
+}