2
|
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 |
}
|