OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

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

}