Dépôt officiel du code source de l'ERP OpenConcerto
Rev 142 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
*
* The contents of this file are subject to the terms of the GNU General Public License Version 3
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each file.
*/
package org.openconcerto.sql.users;
import org.openconcerto.sql.model.SQLRow;
import org.openconcerto.sql.model.SQLRowValues;
import org.openconcerto.sql.model.SQLRowValuesListFetcher;
import org.openconcerto.sql.model.SQLTable;
import org.openconcerto.sql.model.SQLTableEvent;
import org.openconcerto.sql.model.SQLTableModifiedListener;
import org.openconcerto.utils.CompareUtils;
import org.openconcerto.utils.ThreadFactory;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
@ThreadSafe
public class UserManager implements UserSingleton {
public static enum State {
CREATED, VALID, DESTROYED
}
private static final UserSingletonManager<UserManager> sMngr = new UserSingletonManager<UserManager>("USER_COMMON") {
@Override
protected UserManager createInstance(SQLTable t) {
return new UserManager(t).start();
}
};
public static UserSingletonManager<UserManager> getSingletonManager() {
return sMngr;
}
public static UserManager setInstance(final SQLTable t) {
return getSingletonManager().setInstance(t);
}
public static final UserManager getInstance() {
return getSingletonManager().getInstance();
}
public static final User getUser() {
final UserManager mngr = getInstance();
return mngr == null ? null : mngr.getCurrentUser();
}
public static final int getUserID() {
final User user = getUser();
return user == null ? SQLRow.NONEXISTANT_ID : user.getId();
}
// maximum age of DB data
private static final long MAX_AGE_MS = 8 * 60 * 1000;
public static final String CURRENT_USERID_PROPNAME = "currentUserID";
public static final String USERS_PROPNAME = "users";
private final SQLTable t;
@GuardedBy("this")
private Map<Integer, User> byID;
@GuardedBy("this")
private long timeOfLastFill = -1;
// don't store User because of refresh()
@GuardedBy("this")
private Integer currentUserID;
private final SQLTableModifiedListener tableL;
@GuardedBy("this")
private ScheduledFuture<?> scheduledUpdateUsers;
@GuardedBy("this")
private Future<?> updateUsers;
// a user call has placed a runnable to be executed, don't cancel previously added refresh
@GuardedBy("this")
private Future<?> userFuture;
private final ScheduledExecutorService exec;
private final PropertyChangeSupport supp = new PropertyChangeSupport(this);
public UserManager(final SQLTable t) {
this.byID = null;
this.currentUserID = null;
this.t = t;
this.exec = Executors.newScheduledThreadPool(1, new ThreadFactory(UserManager.class.getSimpleName() + " executor for " + t.getSQLName(), true).setPriority(Thread.MIN_PRIORITY));
this.tableL = new SQLTableModifiedListener() {
@Override
public void tableModified(SQLTableEvent evt) {
refresh(true);
}
};
}
public final UserManager start() {
return this.start(MAX_AGE_MS);
}
public final synchronized UserManager start(final long maxAge) {
if (this.getState() != State.CREATED)
throw new IllegalStateException("Already started");
this.t.addTableModifiedListener(this.tableL);
fillUsers();
this.scheduledUpdateUsers = this.exec.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
refreshIfNeeded(maxAge);
}
}, maxAge / 4, maxAge / 4, TimeUnit.MILLISECONDS);
return this;
}
public final boolean isValid() {
return getState() == State.VALID;
}
public synchronized final State getState() {
if (this.scheduledUpdateUsers == null)
return State.CREATED;
else if (this.scheduledUpdateUsers.isDone())
return State.DESTROYED;
else
return State.VALID;
}
@Override
public synchronized final void destroy() {
final State s = this.getState();
if(s == State.DESTROYED)
return;
if (s == State.VALID) {
this.getTable().removeTableModifiedListener(this.tableL);
this.scheduledUpdateUsers.cancel(true);
this.cancelUpdate();
}
this.exec.shutdown();
assert this.getState() == State.DESTROYED;
}
private void cancelUpdate() {
assert Thread.holdsLock(this);
if (this.updateUsers != null && (this.userFuture == null || this.userFuture.isDone()))
this.updateUsers.cancel(true);
}
private boolean isUpdatePending() {
assert Thread.holdsLock(this);
return this.updateUsers != null && !this.updateUsers.isDone();
}
private synchronized void refreshIfNeeded(final long maxAge) {
final long dataAge = System.currentTimeMillis() - this.timeOfLastFill;
// refresh if the data is old enough and no update is pending
if (dataAge > maxAge && !this.isUpdatePending())
refresh(true);
}
public void refresh() {
if (!this.isValid())
throw new IllegalStateException("No longer valid");
this.refresh(false);
}
// don't return the future as it might be canceled despite the fact that a subsequent
// invokeLater() might need it
private synchronized void refresh(final boolean thisClass) {
cancelUpdate();
this.updateUsers = invokeLater(new Callable<Object>() {
@Override
public Object call() throws Exception {
// allow multiple close refresh to be cancelled by the last one
if (thisClass)
Thread.sleep(30);
fillUsers();
return null;
}
}, false);
// the new updateUsers is after userFuture, so we can cancel it
this.userFuture = null;
}
public final <T> Future<T> invokeLater(final Callable<T> c) {
return this.invokeLater(c, true);
}
public synchronized final <T> Future<T> invokeLater(final Callable<T> c, final boolean needsPreviousUpdate) {
if (!this.isValid())
return null;
final Future<T> res = this.exec.submit(c);
if (needsPreviousUpdate) {
this.userFuture = res;
}
return res;
}
private void fillUsers() {
// to keep the ORDER for #getAllUser()
final Map<Integer, User> mutable = new LinkedHashMap<Integer, User>();
final SQLRowValuesListFetcher fetcher = new SQLRowValuesListFetcher(new SQLRowValues(this.t).setAllToNull());
fetcher.setOrdered(true);
for (final SQLRowValues v : fetcher.fetch()) {
final User u = new User(v);
mutable.put(v.getID(), u);
}
final Map<Integer, User> immutable = Collections.unmodifiableMap(mutable);
final Map<Integer, User> old;
synchronized (this) {
old = this.byID;
this.byID = immutable;
this.timeOfLastFill = System.currentTimeMillis();
}
this.supp.firePropertyChange(USERS_PROPNAME, old, immutable);
}
@Override
public final SQLTable getTable() {
return this.t;
}
/**
* All users indexed by their IDs.
*
* @return all users.
*/
public synchronized final Map<Integer, User> getUsers() {
if (this.byID == null)
throw new IllegalStateException(this + " wasn't started successfully");
return this.byID;
}
public final void addUsersListener(final PropertyChangeListener l) {
this.supp.addPropertyChangeListener(USERS_PROPNAME, l);
}
public final void removeUsersListener(final PropertyChangeListener l) {
this.supp.removePropertyChangeListener(USERS_PROPNAME, l);
}
public synchronized final User getCurrentUser() {
return this.currentUserID == null ? null : this.getUser(this.currentUserID);
}
public synchronized final Integer getCurrentUserID() {
return this.currentUserID;
}
@Deprecated
public void setCurrentUser(final Number id) {
this.setCurrentUserID(id);
}
public void setCurrentUserID(final Number id) {
final Integer newVal = id == null || id instanceof Integer ? (Integer) id : Integer.valueOf(id.intValue());
final boolean modified;
synchronized (this) {
modified = !CompareUtils.equals(this.currentUserID, newVal);
if (modified)
this.currentUserID = newVal;
}
if (modified)
this.supp.firePropertyChange(CURRENT_USERID_PROPNAME, null, newVal);
}
public final void addCurrentUserIDListener(final PropertyChangeListener l) {
this.supp.addPropertyChangeListener(CURRENT_USERID_PROPNAME, l);
}
public final void removeCurrentUserIDListener(final PropertyChangeListener l) {
this.supp.removePropertyChangeListener(CURRENT_USERID_PROPNAME, l);
}
public List<User> getAllActiveUsers() {
final List<User> result = new ArrayList<User>();
for (User user : this.getUsers().values()) {
if (user.isActive()) {
result.add(user);
}
}
return result;
}
public User getUser(final Integer v) {
final Map<Integer, User> users = this.getUsers();
if (users.containsKey(v))
return users.get(v);
else
throw new IllegalStateException("Bad user! " + v);
}
}