org.chromium.sdk/src/org/chromium/sdk/internal/SessionManager.java
changeset 2 e4420d2515f1
equal deleted inserted replaced
1:ef76fc2ac88c 2:e4420d2515f1
       
     1 // Copyright (c) 2009 The Chromium Authors. All rights reserved.
       
     2 // Use of this source code is governed by a BSD-style license that can be
       
     3 // found in the LICENSE file.
       
     4 
       
     5 package org.chromium.sdk.internal;
       
     6 
       
     7 import java.util.ArrayList;
       
     8 import java.util.Collection;
       
     9 import java.util.Collections;
       
    10 import java.util.List;
       
    11 
       
    12 /**
       
    13  * Manager that switches on and off some resource for a shared multiuser access.
       
    14  * The nature of actual resource should be defined in subclass of
       
    15  * {@link SessionManager}. Time period when resource is on is called "session".
       
    16  * Switch on operation (aka session creation) must be an atomic operation.
       
    17  * Switch off (aka session closing) may be lengthy asynchronous operation.
       
    18  * <p>
       
    19  * If no user needs it, manager switches the resource off. On the first demand
       
    20  * resource gets switched on (and new session gets created). After the last user
       
    21  * has released the resource, the session finishes either instantly or
       
    22  * some time later. In the latter case resource becomes temporary unavailable.
       
    23  * The manager does not operate resource in any other sense than switching it
       
    24  * on and off.
       
    25  * <p>
       
    26  * Every user first acquires the resource by calling {@link #connect()} method.
       
    27  * It gets ticket which points to the corresponding session. Method
       
    28  * {@link Ticket#dismiss()} must be called when resource is no more needed.
       
    29  * @param <SESSION> user class that represents a session; must
       
    30  *                  extend {@link SessionBase}
       
    31  * @param <EX> exception that is allowed to be thrown when resource is being switched
       
    32  *             on and the new session is starting; {@link RuntimeException}
       
    33  *             is a good default parameter value
       
    34  */
       
    35 public abstract class SessionManager<SESSION extends SessionManager.SessionBase<SESSION>,
       
    36     EX extends Exception> {
       
    37 
       
    38   // Holds current session; all access must be synchronized on "this".
       
    39   private SESSION currentSession = null;
       
    40 
       
    41   /**
       
    42    * Ticket to resource use. Every client gets its own copy. All tickets must
       
    43    * be dismissed in order for resource to be switched off.
       
    44    * @param <SESSION> is be the same type as of manager that issued this ticket
       
    45    */
       
    46   public interface Ticket<SESSION> {
       
    47     /**
       
    48      * Each valid ticket points to session of the resource. The actual type
       
    49      * {@code SESSION} is provided by user (as a type parameter of enclosing
       
    50      * SessionManager). The actual resource should be accessible from
       
    51      * {@code SESSION}.
       
    52      * @return non-null current session
       
    53      * @throws IllegalStateException if ticket is no longer valid
       
    54      */
       
    55     SESSION getSession();
       
    56 
       
    57     /**
       
    58      * Releases resource and makes ticket invalid. Switches the resource
       
    59      * off if it was a last ticket.
       
    60      * @throws IllegalStateException if ticket is no more valid
       
    61      */
       
    62     void dismiss();
       
    63   }
       
    64 
       
    65   /**
       
    66    * Registers user request for resource and switches the resource on if required.
       
    67    * @return new ticket which symbolize use of resource until
       
    68    *             {@link Ticket#dismiss()} is called
       
    69    * @throws EX if connect required creating a new session and if the new session creation
       
    70    *         has failed
       
    71    */
       
    72   public Ticket<SESSION> connect() throws EX {
       
    73     synchronized (this) {
       
    74       if (currentSession != null) {
       
    75         // this may reset currentSession
       
    76         currentSession.checkHealth();
       
    77       }
       
    78       if (currentSession == null) {
       
    79         currentSession = newSessionObject();
       
    80         if (currentSession.manager != this) {
       
    81           throw new IllegalArgumentException("Wrong manager was set in session");
       
    82         }
       
    83       }
       
    84       return currentSession.newTicket();
       
    85     }
       
    86   }
       
    87 
       
    88   /**
       
    89    * User-provided constructor of a new session. It should switch the resource on
       
    90    * whatever it actually means.
       
    91    * @return new instance of resource use session
       
    92    * @throws EX if switching resource on or creating a new session failed
       
    93    */
       
    94   protected abstract SESSION newSessionObject() throws EX;
       
    95 
       
    96   /**
       
    97    * Base class for user session. It should be subclassed and it is parameterized by
       
    98    * this subclass. Object construction should have semantics of switching resource
       
    99    * on. It gets constructed via user-defined {@link SessionManager#newSessionObject()}.
       
   100    * Subclass should honestly pass instance of {@link SessionManager} to the base
       
   101    * class. User also should implement {@link #lastTicketDismissed()} and helper
       
   102    * {@link #getThisAsSession()}.
       
   103    * @param <SESSION> the very user class which extends {@link SessionBase};
       
   104    *                  {@link #getThisAsSession()} should compile as "return this;"
       
   105    */
       
   106   public static abstract class SessionBase<SESSION extends SessionBase<SESSION>> {
       
   107     private final SessionManager<?, ?> manager;
       
   108     private boolean isConnectionStopped = false;
       
   109     private boolean isCancelled = false;
       
   110 
       
   111     SessionBase(SessionManager<SESSION, ?> manager) {
       
   112       this.manager = manager;
       
   113     }
       
   114 
       
   115     /**
       
   116      * Must be simply "return this;"
       
   117      */
       
   118     protected abstract SESSION getThisAsSession();
       
   119 
       
   120     /**
       
   121      * Session may check its health here. This check is made on
       
   122      * every new connection. If it appears that the session is no longer alive
       
   123      * the method should call {@link #interruptSession()}. However, this is a highly
       
   124      * unwanted scenario: session should interrupt itself synchronously, no
       
   125      * on-demand from this method.
       
   126      */
       
   127     protected abstract void checkHealth();
       
   128 
       
   129     /**
       
   130      * User-provided behavior when no more valid tickets left. Resource should
       
   131      * be switched off whatever it actually means and the session closed.
       
   132      * There are 3 options here:
       
   133      * <ol>
       
   134      * <li>Method is finished with {@link #closeSession()} call. Method
       
   135      * {@link SessionManager#connect()} does not interrupt its service and simply
       
   136      * creates new session the next call.
       
   137      * <li>Method is finished with {@link #stopNewConnections()} call. Connection
       
   138      * process is put on hold after this and {@link SessionManager#connect()} starts
       
   139      * to throw {@link IllegalStateException}. Later {@link #closeSession()} must
       
   140      * be called possibly asynchronously. After this the resource is available again
       
   141      * and a new session may be created.
       
   142      * <li>Do not call any of methods listed above. This probably works but is
       
   143      * not specified here.
       
   144      * </ol>
       
   145      */
       
   146     protected abstract void lastTicketDismissed();
       
   147 
       
   148     /**
       
   149      * See {@link #lastTicketDismissed()}. This method is supposed to be called
       
   150      * from there, but not necessarily.
       
   151      */
       
   152     protected void stopNewConnections() {
       
   153       synchronized (manager) {
       
   154         isConnectionStopped = true;
       
   155       }
       
   156     }
       
   157 
       
   158     /**
       
   159      * Stops all new connections and cancels all existing tickets. Don't forget
       
   160      * to call {@link #closeSession()} manually.
       
   161      * @return collection of exceptions we gathered from tickets
       
   162      */
       
   163     protected Collection<? extends RuntimeException> interruptSession() {
       
   164       synchronized (manager) {
       
   165         isConnectionStopped = true;
       
   166         isCancelled = true;
       
   167         // TODO(peter.rybin): notify listeners here in case they are interested
       
   168         tickets.clear();
       
   169       }
       
   170 
       
   171       return Collections.emptyList();
       
   172     }
       
   173 
       
   174     /**
       
   175      * See {@link #lastTicketDismissed()}. This method is supposed to be called
       
   176      * from there, but not necessarily.
       
   177      */
       
   178     protected void closeSession() {
       
   179       synchronized (manager) {
       
   180         isConnectionStopped = true;
       
   181         if (!tickets.isEmpty()) {
       
   182           throw new IllegalStateException("Some tickets are still valid");
       
   183         }
       
   184         if (manager.currentSession != this) {
       
   185           throw new IllegalStateException("Session is not active");
       
   186         }
       
   187         manager.currentSession = null;
       
   188       }
       
   189     }
       
   190 
       
   191     /**
       
   192      * Creates new ticket that is to be dismissed later.
       
   193      * Internal method. However user may use it or even make it public.
       
   194      */
       
   195     protected Ticket<SESSION> newTicket() {
       
   196       synchronized (manager) {
       
   197         if (isConnectionStopped) {
       
   198           throw new IllegalStateException("Connection has been stopped");
       
   199         }
       
   200         TicketImpl ticketImpl = new TicketImpl();
       
   201         tickets.add(ticketImpl);
       
   202         return ticketImpl;
       
   203       }
       
   204     }
       
   205 
       
   206     private final List<TicketImpl> tickets = new ArrayList<TicketImpl>();
       
   207 
       
   208     private class TicketImpl implements Ticket<SESSION> {
       
   209       private volatile boolean isDismissed = false;
       
   210 
       
   211       public void dismiss() {
       
   212         synchronized (manager) {
       
   213           if (!isCancelled) {
       
   214             boolean res = tickets.remove(this);
       
   215             if (!res) {
       
   216               throw new IllegalStateException("Ticket is already dismissed");
       
   217             }
       
   218             if (tickets.isEmpty()) {
       
   219               lastTicketDismissed();
       
   220             }
       
   221           }
       
   222           isDismissed = true;
       
   223         }
       
   224       }
       
   225 
       
   226       public SESSION getSession() {
       
   227         if (isDismissed) {
       
   228           throw new IllegalStateException("Ticket is dismissed");
       
   229         }
       
   230         return getThisAsSession();
       
   231       }
       
   232     }
       
   233   }
       
   234 
       
   235   /**
       
   236    * This method is completely unsynchronized. Is should be used for
       
   237    * single-threaded tests only.
       
   238    */
       
   239   public SESSION getCurrentSessionForTest() {
       
   240     return currentSession;
       
   241   }
       
   242 }