17 |
ilm |
1 |
/*
|
|
|
2 |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
|
|
|
3 |
*
|
182 |
ilm |
4 |
* Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
|
17 |
ilm |
5 |
*
|
|
|
6 |
* The contents of this file are subject to the terms of the GNU General Public License Version 3
|
|
|
7 |
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
|
|
|
8 |
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
|
|
|
9 |
* language governing permissions and limitations under the License.
|
|
|
10 |
*
|
|
|
11 |
* When distributing the software, include this License Header Notice in each file.
|
|
|
12 |
*/
|
|
|
13 |
|
|
|
14 |
package org.openconcerto.sql.users;
|
|
|
15 |
|
|
|
16 |
import org.openconcerto.sql.model.SQLRow;
|
|
|
17 |
import org.openconcerto.sql.model.SQLRowValues;
|
|
|
18 |
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
|
|
|
19 |
import org.openconcerto.sql.model.SQLTable;
|
|
|
20 |
import org.openconcerto.sql.model.SQLTableEvent;
|
|
|
21 |
import org.openconcerto.sql.model.SQLTableModifiedListener;
|
144 |
ilm |
22 |
import org.openconcerto.utils.CompareUtils;
|
|
|
23 |
import org.openconcerto.utils.ThreadFactory;
|
17 |
ilm |
24 |
|
144 |
ilm |
25 |
import java.beans.PropertyChangeListener;
|
|
|
26 |
import java.beans.PropertyChangeSupport;
|
17 |
ilm |
27 |
import java.util.ArrayList;
|
142 |
ilm |
28 |
import java.util.Collections;
|
17 |
ilm |
29 |
import java.util.LinkedHashMap;
|
|
|
30 |
import java.util.List;
|
|
|
31 |
import java.util.Map;
|
144 |
ilm |
32 |
import java.util.concurrent.Callable;
|
|
|
33 |
import java.util.concurrent.Executors;
|
|
|
34 |
import java.util.concurrent.Future;
|
|
|
35 |
import java.util.concurrent.ScheduledExecutorService;
|
|
|
36 |
import java.util.concurrent.ScheduledFuture;
|
|
|
37 |
import java.util.concurrent.TimeUnit;
|
17 |
ilm |
38 |
|
142 |
ilm |
39 |
import net.jcip.annotations.GuardedBy;
|
|
|
40 |
import net.jcip.annotations.ThreadSafe;
|
|
|
41 |
|
|
|
42 |
@ThreadSafe
|
144 |
ilm |
43 |
public class UserManager implements UserSingleton {
|
17 |
ilm |
44 |
|
144 |
ilm |
45 |
public static enum State {
|
|
|
46 |
CREATED, VALID, DESTROYED
|
|
|
47 |
}
|
|
|
48 |
|
|
|
49 |
private static final UserSingletonManager<UserManager> sMngr = new UserSingletonManager<UserManager>("USER_COMMON") {
|
|
|
50 |
@Override
|
|
|
51 |
protected UserManager createInstance(SQLTable t) {
|
|
|
52 |
return new UserManager(t).start();
|
17 |
ilm |
53 |
}
|
144 |
ilm |
54 |
};
|
|
|
55 |
|
|
|
56 |
public static UserSingletonManager<UserManager> getSingletonManager() {
|
|
|
57 |
return sMngr;
|
17 |
ilm |
58 |
}
|
|
|
59 |
|
144 |
ilm |
60 |
public static UserManager setInstance(final SQLTable t) {
|
|
|
61 |
return getSingletonManager().setInstance(t);
|
|
|
62 |
}
|
|
|
63 |
|
|
|
64 |
public static final UserManager getInstance() {
|
|
|
65 |
return getSingletonManager().getInstance();
|
|
|
66 |
}
|
|
|
67 |
|
73 |
ilm |
68 |
public static final User getUser() {
|
17 |
ilm |
69 |
final UserManager mngr = getInstance();
|
182 |
ilm |
70 |
return getCurrentUser(mngr);
|
|
|
71 |
}
|
|
|
72 |
|
|
|
73 |
public static User getCurrentUser(final UserManager mngr) {
|
73 |
ilm |
74 |
return mngr == null ? null : mngr.getCurrentUser();
|
17 |
ilm |
75 |
}
|
|
|
76 |
|
73 |
ilm |
77 |
public static final int getUserID() {
|
|
|
78 |
final User user = getUser();
|
|
|
79 |
return user == null ? SQLRow.NONEXISTANT_ID : user.getId();
|
|
|
80 |
}
|
|
|
81 |
|
144 |
ilm |
82 |
// maximum age of DB data
|
|
|
83 |
private static final long MAX_AGE_MS = 8 * 60 * 1000;
|
|
|
84 |
public static final String CURRENT_USERID_PROPNAME = "currentUserID";
|
|
|
85 |
public static final String USERS_PROPNAME = "users";
|
|
|
86 |
|
17 |
ilm |
87 |
private final SQLTable t;
|
142 |
ilm |
88 |
@GuardedBy("this")
|
|
|
89 |
private Map<Integer, User> byID;
|
|
|
90 |
@GuardedBy("this")
|
144 |
ilm |
91 |
private long timeOfLastFill = -1;
|
|
|
92 |
// don't store User because of refresh()
|
142 |
ilm |
93 |
@GuardedBy("this")
|
144 |
ilm |
94 |
private Integer currentUserID;
|
|
|
95 |
private final SQLTableModifiedListener tableL;
|
|
|
96 |
@GuardedBy("this")
|
|
|
97 |
private ScheduledFuture<?> scheduledUpdateUsers;
|
|
|
98 |
@GuardedBy("this")
|
|
|
99 |
private Future<?> updateUsers;
|
|
|
100 |
// a user call has placed a runnable to be executed, don't cancel previously added refresh
|
|
|
101 |
@GuardedBy("this")
|
|
|
102 |
private Future<?> userFuture;
|
|
|
103 |
private final ScheduledExecutorService exec;
|
17 |
ilm |
104 |
|
144 |
ilm |
105 |
private final PropertyChangeSupport supp = new PropertyChangeSupport(this);
|
|
|
106 |
|
|
|
107 |
public UserManager(final SQLTable t) {
|
|
|
108 |
this.byID = null;
|
|
|
109 |
this.currentUserID = null;
|
17 |
ilm |
110 |
this.t = t;
|
144 |
ilm |
111 |
this.exec = Executors.newScheduledThreadPool(1, new ThreadFactory(UserManager.class.getSimpleName() + " executor for " + t.getSQLName(), true).setPriority(Thread.MIN_PRIORITY));
|
|
|
112 |
this.tableL = new SQLTableModifiedListener() {
|
17 |
ilm |
113 |
@Override
|
|
|
114 |
public void tableModified(SQLTableEvent evt) {
|
144 |
ilm |
115 |
refresh(true);
|
17 |
ilm |
116 |
}
|
144 |
ilm |
117 |
};
|
|
|
118 |
}
|
|
|
119 |
|
|
|
120 |
public final UserManager start() {
|
|
|
121 |
return this.start(MAX_AGE_MS);
|
|
|
122 |
}
|
|
|
123 |
|
|
|
124 |
public final synchronized UserManager start(final long maxAge) {
|
|
|
125 |
if (this.getState() != State.CREATED)
|
|
|
126 |
throw new IllegalStateException("Already started");
|
|
|
127 |
this.t.addTableModifiedListener(this.tableL);
|
17 |
ilm |
128 |
fillUsers();
|
144 |
ilm |
129 |
this.scheduledUpdateUsers = this.exec.scheduleWithFixedDelay(new Runnable() {
|
|
|
130 |
@Override
|
|
|
131 |
public void run() {
|
|
|
132 |
refreshIfNeeded(maxAge);
|
|
|
133 |
}
|
|
|
134 |
}, maxAge / 4, maxAge / 4, TimeUnit.MILLISECONDS);
|
|
|
135 |
return this;
|
17 |
ilm |
136 |
}
|
|
|
137 |
|
144 |
ilm |
138 |
public final boolean isValid() {
|
|
|
139 |
return getState() == State.VALID;
|
142 |
ilm |
140 |
}
|
|
|
141 |
|
144 |
ilm |
142 |
public synchronized final State getState() {
|
|
|
143 |
if (this.scheduledUpdateUsers == null)
|
|
|
144 |
return State.CREATED;
|
|
|
145 |
else if (this.scheduledUpdateUsers.isDone())
|
|
|
146 |
return State.DESTROYED;
|
|
|
147 |
else
|
|
|
148 |
return State.VALID;
|
|
|
149 |
}
|
|
|
150 |
|
|
|
151 |
@Override
|
|
|
152 |
public synchronized final void destroy() {
|
|
|
153 |
final State s = this.getState();
|
|
|
154 |
if(s == State.DESTROYED)
|
|
|
155 |
return;
|
|
|
156 |
if (s == State.VALID) {
|
|
|
157 |
this.getTable().removeTableModifiedListener(this.tableL);
|
|
|
158 |
this.scheduledUpdateUsers.cancel(true);
|
|
|
159 |
this.cancelUpdate();
|
|
|
160 |
}
|
|
|
161 |
this.exec.shutdown();
|
|
|
162 |
assert this.getState() == State.DESTROYED;
|
|
|
163 |
}
|
|
|
164 |
|
|
|
165 |
private void cancelUpdate() {
|
|
|
166 |
assert Thread.holdsLock(this);
|
|
|
167 |
if (this.updateUsers != null && (this.userFuture == null || this.userFuture.isDone()))
|
|
|
168 |
this.updateUsers.cancel(true);
|
|
|
169 |
}
|
|
|
170 |
|
|
|
171 |
private boolean isUpdatePending() {
|
|
|
172 |
assert Thread.holdsLock(this);
|
|
|
173 |
return this.updateUsers != null && !this.updateUsers.isDone();
|
|
|
174 |
}
|
|
|
175 |
|
|
|
176 |
private synchronized void refreshIfNeeded(final long maxAge) {
|
|
|
177 |
final long dataAge = System.currentTimeMillis() - this.timeOfLastFill;
|
|
|
178 |
// refresh if the data is old enough and no update is pending
|
|
|
179 |
if (dataAge > maxAge && !this.isUpdatePending())
|
|
|
180 |
refresh(true);
|
|
|
181 |
}
|
|
|
182 |
|
|
|
183 |
public void refresh() {
|
|
|
184 |
if (!this.isValid())
|
|
|
185 |
throw new IllegalStateException("No longer valid");
|
|
|
186 |
this.refresh(false);
|
|
|
187 |
}
|
|
|
188 |
|
|
|
189 |
// don't return the future as it might be canceled despite the fact that a subsequent
|
|
|
190 |
// invokeLater() might need it
|
|
|
191 |
private synchronized void refresh(final boolean thisClass) {
|
|
|
192 |
cancelUpdate();
|
|
|
193 |
this.updateUsers = invokeLater(new Callable<Object>() {
|
|
|
194 |
@Override
|
|
|
195 |
public Object call() throws Exception {
|
|
|
196 |
// allow multiple close refresh to be cancelled by the last one
|
|
|
197 |
if (thisClass)
|
|
|
198 |
Thread.sleep(30);
|
|
|
199 |
fillUsers();
|
|
|
200 |
return null;
|
|
|
201 |
}
|
|
|
202 |
}, false);
|
|
|
203 |
// the new updateUsers is after userFuture, so we can cancel it
|
|
|
204 |
this.userFuture = null;
|
|
|
205 |
}
|
|
|
206 |
|
|
|
207 |
public final <T> Future<T> invokeLater(final Callable<T> c) {
|
|
|
208 |
return this.invokeLater(c, true);
|
|
|
209 |
}
|
|
|
210 |
|
|
|
211 |
public synchronized final <T> Future<T> invokeLater(final Callable<T> c, final boolean needsPreviousUpdate) {
|
|
|
212 |
if (!this.isValid())
|
|
|
213 |
return null;
|
|
|
214 |
final Future<T> res = this.exec.submit(c);
|
|
|
215 |
if (needsPreviousUpdate) {
|
|
|
216 |
this.userFuture = res;
|
|
|
217 |
}
|
|
|
218 |
return res;
|
|
|
219 |
}
|
|
|
220 |
|
|
|
221 |
private void fillUsers() {
|
142 |
ilm |
222 |
// to keep the ORDER for #getAllUser()
|
|
|
223 |
final Map<Integer, User> mutable = new LinkedHashMap<Integer, User>();
|
17 |
ilm |
224 |
final SQLRowValuesListFetcher fetcher = new SQLRowValuesListFetcher(new SQLRowValues(this.t).setAllToNull());
|
|
|
225 |
fetcher.setOrdered(true);
|
|
|
226 |
for (final SQLRowValues v : fetcher.fetch()) {
|
142 |
ilm |
227 |
final User u = new User(v);
|
|
|
228 |
mutable.put(v.getID(), u);
|
17 |
ilm |
229 |
}
|
144 |
ilm |
230 |
final Map<Integer, User> immutable = Collections.unmodifiableMap(mutable);
|
|
|
231 |
final Map<Integer, User> old;
|
|
|
232 |
synchronized (this) {
|
|
|
233 |
old = this.byID;
|
|
|
234 |
this.byID = immutable;
|
|
|
235 |
this.timeOfLastFill = System.currentTimeMillis();
|
|
|
236 |
}
|
|
|
237 |
this.supp.firePropertyChange(USERS_PROPNAME, old, immutable);
|
17 |
ilm |
238 |
}
|
|
|
239 |
|
144 |
ilm |
240 |
@Override
|
17 |
ilm |
241 |
public final SQLTable getTable() {
|
|
|
242 |
return this.t;
|
|
|
243 |
}
|
|
|
244 |
|
144 |
ilm |
245 |
/**
|
|
|
246 |
* All users indexed by their IDs.
|
|
|
247 |
*
|
|
|
248 |
* @return all users.
|
|
|
249 |
*/
|
|
|
250 |
public synchronized final Map<Integer, User> getUsers() {
|
|
|
251 |
if (this.byID == null)
|
|
|
252 |
throw new IllegalStateException(this + " wasn't started successfully");
|
17 |
ilm |
253 |
return this.byID;
|
|
|
254 |
}
|
|
|
255 |
|
144 |
ilm |
256 |
public final void addUsersListener(final PropertyChangeListener l) {
|
|
|
257 |
this.supp.addPropertyChangeListener(USERS_PROPNAME, l);
|
|
|
258 |
}
|
|
|
259 |
|
|
|
260 |
public final void removeUsersListener(final PropertyChangeListener l) {
|
|
|
261 |
this.supp.removePropertyChangeListener(USERS_PROPNAME, l);
|
|
|
262 |
}
|
|
|
263 |
|
142 |
ilm |
264 |
public synchronized final User getCurrentUser() {
|
144 |
ilm |
265 |
return this.currentUserID == null ? null : this.getUser(this.currentUserID);
|
17 |
ilm |
266 |
}
|
|
|
267 |
|
144 |
ilm |
268 |
public synchronized final Integer getCurrentUserID() {
|
|
|
269 |
return this.currentUserID;
|
17 |
ilm |
270 |
}
|
|
|
271 |
|
144 |
ilm |
272 |
@Deprecated
|
|
|
273 |
public void setCurrentUser(final Number id) {
|
|
|
274 |
this.setCurrentUserID(id);
|
17 |
ilm |
275 |
}
|
|
|
276 |
|
144 |
ilm |
277 |
public void setCurrentUserID(final Number id) {
|
|
|
278 |
final Integer newVal = id == null || id instanceof Integer ? (Integer) id : Integer.valueOf(id.intValue());
|
|
|
279 |
final boolean modified;
|
|
|
280 |
synchronized (this) {
|
|
|
281 |
modified = !CompareUtils.equals(this.currentUserID, newVal);
|
|
|
282 |
if (modified)
|
|
|
283 |
this.currentUserID = newVal;
|
|
|
284 |
}
|
|
|
285 |
if (modified)
|
|
|
286 |
this.supp.firePropertyChange(CURRENT_USERID_PROPNAME, null, newVal);
|
|
|
287 |
}
|
|
|
288 |
|
|
|
289 |
public final void addCurrentUserIDListener(final PropertyChangeListener l) {
|
|
|
290 |
this.supp.addPropertyChangeListener(CURRENT_USERID_PROPNAME, l);
|
|
|
291 |
}
|
|
|
292 |
|
|
|
293 |
public final void removeCurrentUserIDListener(final PropertyChangeListener l) {
|
|
|
294 |
this.supp.removePropertyChangeListener(CURRENT_USERID_PROPNAME, l);
|
|
|
295 |
}
|
|
|
296 |
|
142 |
ilm |
297 |
public List<User> getAllActiveUsers() {
|
|
|
298 |
final List<User> result = new ArrayList<User>();
|
|
|
299 |
for (User user : this.getUsers().values()) {
|
132 |
ilm |
300 |
if (user.isActive()) {
|
|
|
301 |
result.add(user);
|
|
|
302 |
}
|
|
|
303 |
}
|
|
|
304 |
return result;
|
|
|
305 |
}
|
|
|
306 |
|
142 |
ilm |
307 |
public User getUser(final Integer v) {
|
|
|
308 |
final Map<Integer, User> users = this.getUsers();
|
|
|
309 |
if (users.containsKey(v))
|
|
|
310 |
return users.get(v);
|
17 |
ilm |
311 |
else
|
|
|
312 |
throw new IllegalStateException("Bad user! " + v);
|
|
|
313 |
}
|
|
|
314 |
|
|
|
315 |
}
|