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