// 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.shellprotocol; 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>> { protected 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; } }