PresenceManager.java

/*
 * The coLAB project
 * Copyright (C) 2022-2023 AlbaSim, MEI, HEIG-VD, HES-SO
 *
 * Licensed under the MIT License
 */
package ch.colabproject.colab.api.presence;

import ch.colabproject.colab.api.controller.EntityGatheringBagForPropagation;
import ch.colabproject.colab.api.controller.RequestManager;
import ch.colabproject.colab.api.controller.project.ProjectManager;
import ch.colabproject.colab.api.controller.team.TeamManager;
import ch.colabproject.colab.api.model.project.Project;
import ch.colabproject.colab.api.model.team.TeamMember;
import ch.colabproject.colab.api.model.user.User;
import ch.colabproject.colab.api.presence.model.TouchUserPresence;
import ch.colabproject.colab.api.presence.model.UserPresence;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.cp.lock.FencedLock;
import com.hazelcast.map.IMap;

/**
 * To manages user presence
 *
 * @author maxence
 */
@Stateless
@LocalBean
public class PresenceManager {

    /** logger */
    private static final Logger logger = LoggerFactory.getLogger(PresenceManager.class);

    /** Hazelcast instance. */
    @Inject
    private HazelcastInstance hzInstance;

    /**
     * To register presence
     */
    @Inject
    private EntityGatheringBagForPropagation transactionManager;

    /** Project specific logic management */
    @Inject
    private ProjectManager projectManager;

    /** To fetch teamMembers */
    @Inject
    private TeamManager teamManager;

    /** to fetch current user */
    @Inject
    private RequestManager requestManager;

    /**
     * Get shared cache of presence.
     * <p>
     * projectId => wsSessionId => UserPresence
     */
    private IMap<Long, Map<String, UserPresence>> getCache() {
        return hzInstance.getMap("PRESENCE_CACHE");
    }

    /**
     * Get the lock for the given project id
     *
     * @param id id of the project
     *
     * @return the lock
     */
    private FencedLock getLock(Long id) {
        return hzInstance.getCPSubsystem().getLock("Project-Presence-" + id);
    }

    /**
     * Get presence list for the current project
     *
     * @param projectId if of the project
     *
     * @return presence list
     */
    public Collection<UserPresence> getPresenceList(Long projectId) {
        // just to check read access to project
        projectManager.assertAndGetProject(projectId);

        Map<String, UserPresence> get = null;
        try {
            get = getCache().get(projectId);
        } catch (RuntimeException e) {
            logger.warn("Unable to fetch presence list", e);
        }

        if (get != null) {
            return get.values();
        } else {
            return new HashSet<>();
        }
    }

    /**
     * Register and propagate user presence
     *
     * @param touch presence data
     */
    public void updateUserPresence(TouchUserPresence touch) {
        if (touch != null && touch.getProjectId() != null && touch.getWsSessionId() != null) {
            Long projectId = touch.getProjectId();

            Project project = projectManager.assertAndGetProject(projectId);
            User currentUser = requestManager.getCurrentUser();
            TeamMember member = teamManager.findMemberByProjectAndUser(project, currentUser);

            String wsSessionId = touch.getWsSessionId();
            FencedLock lock = getLock(projectId);
            UserPresence userPresence = new UserPresence(touch);

            // no member => user is an admin
            if (member != null) {
                userPresence.setTeamMemberId(member.getId());
            }

            try {
                lock.lock();
                IMap<Long, Map<String, UserPresence>> cache = getCache();
                cache.putIfAbsent(projectId, new HashMap<>());
                Map<String, UserPresence> projectPresence = cache.get(projectId);
                projectPresence.put(wsSessionId, userPresence);
                cache.put(projectId, projectPresence);

                transactionManager.registerUpdate(userPresence);
            } catch (RuntimeException e) {
                logger.warn("Unable to update user presence", e);
            } finally {
                lock.unlock();
            }
        }
    }

    /**
     * User has just left, clear from activity table and propagate.
     *
     * @param projectId   id of the project
     * @param wsSessionId the sessionId to clean
     */
    public void clearWsSession(Long projectId, String wsSessionId) {

        FencedLock lock = getLock(projectId);
        try {
            lock.lock();
            IMap<Long, Map<String, UserPresence>> cache = getCache();
            Map<String, UserPresence> projectPresence = cache.get(projectId);

            if (projectPresence != null) {
                UserPresence remove = projectPresence.remove(wsSessionId);
                transactionManager.registerDeletion(remove);
                if (projectPresence.isEmpty()) {
                    cache.delete(projectId);
                } else {
                    // make sure to cal setValue to save the change!
                    cache.put(projectId, projectPresence);
                }
            }
        } catch (RuntimeException e) {
            logger.warn("Unable to remove wsSession", e);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Clear presence list for the given project
     *
     * @param projectId id of the project
     */
    public void clearProjectPresenceList(Long projectId) {
        // laod project to check permissions
        projectManager.assertAndGetProject(projectId);

        FencedLock lock = getLock(projectId);

        try {
            lock.lock();
            getCache().delete(projectId);
        } finally {
            lock.unlock();
        }
    }

    /**
     * Clear all presence lists
     */
    public void clearAllPresenceLists() {
        if (requestManager.getCurrentUser().isAdmin()) {
            getCache().evictAll();
        }
    }
}