de.thm.arsnova.socket.ARSnovaSocketIOServer.java Source code

Java tutorial

Introduction

Here is the source code for de.thm.arsnova.socket.ARSnovaSocketIOServer.java

Source

/*
 * This file is part of ARSnova Backend.
 * Copyright (C) 2012-2015 The ARSnova Team
 *
 * ARSnova Backend is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ARSnova Backend is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.thm.arsnova.socket;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;

import javax.annotation.PreDestroy;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import com.corundumstudio.socketio.AckRequest;
import com.corundumstudio.socketio.Configuration;
import com.corundumstudio.socketio.SocketConfig;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.listener.ConnectListener;
import com.corundumstudio.socketio.listener.DataListener;
import com.corundumstudio.socketio.listener.DisconnectListener;
import com.corundumstudio.socketio.protocol.Packet;
import com.corundumstudio.socketio.protocol.PacketType;

import de.thm.arsnova.entities.InterposedQuestion;
import de.thm.arsnova.entities.User;
import de.thm.arsnova.entities.transport.LearningProgressType;
import de.thm.arsnova.events.ChangeLearningProgress;
import de.thm.arsnova.events.DeleteAllLectureAnswersEvent;
import de.thm.arsnova.events.DeleteAllPreparationAnswersEvent;
import de.thm.arsnova.events.DeleteAllQuestionsAnswersEvent;
import de.thm.arsnova.events.DeleteAnswerEvent;
import de.thm.arsnova.events.DeleteFeedbackForSessionsEvent;
import de.thm.arsnova.events.DeleteInterposedQuestionEvent;
import de.thm.arsnova.events.DeleteQuestionEvent;
import de.thm.arsnova.events.NewAnswerEvent;
import de.thm.arsnova.events.NewFeedbackEvent;
import de.thm.arsnova.events.NewInterposedQuestionEvent;
import de.thm.arsnova.events.NewQuestionEvent;
import de.thm.arsnova.events.NovaEventVisitor;
import de.thm.arsnova.events.PiRoundDelayedStartEvent;
import de.thm.arsnova.events.PiRoundEndEvent;
import de.thm.arsnova.events.StatusSessionEvent;
import de.thm.arsnova.exceptions.UnauthorizedException;
import de.thm.arsnova.exceptions.NoContentException;
import de.thm.arsnova.exceptions.NotFoundException;
import de.thm.arsnova.services.IFeedbackService;
import de.thm.arsnova.services.IQuestionService;
import de.thm.arsnova.services.ISessionService;
import de.thm.arsnova.services.IUserService;
import de.thm.arsnova.socket.message.Feedback;
import de.thm.arsnova.socket.message.Question;
import de.thm.arsnova.socket.message.Session;

@Component
public class ARSnovaSocketIOServer implements ARSnovaSocket, NovaEventVisitor {

    @Autowired
    private IFeedbackService feedbackService;

    @Autowired
    private IUserService userService;

    @Autowired
    private ISessionService sessionService;

    @Autowired
    private IQuestionService questionService;

    private static final Logger LOGGER = LoggerFactory.getLogger(ARSnovaSocketIOServer.class);

    private int portNumber;
    private String hostIp;
    private boolean useSSL = false;
    private String keystore;
    private String storepass;
    private final Configuration config;
    private SocketIOServer server;

    public ARSnovaSocketIOServer() {
        config = new Configuration();
    }

    @PreDestroy
    public void closeAllSessions() {
        LOGGER.info("Close all websockets due to @PreDestroy");
        for (final SocketIOClient c : server.getAllClients()) {
            c.disconnect();
        }

        int clientCount = 0;
        for (final SocketIOClient c : server.getAllClients()) {
            c.send(new Packet(PacketType.DISCONNECT));
            clientCount++;
        }
        LOGGER.info("Pending websockets at @PreDestroy: {}", clientCount);
        server.stop();
    }

    public void startServer() {
        /**
         * hack: listen to ipv4 adresses
         */
        System.setProperty("java.net.preferIPv4Stack", "true");

        SocketConfig soConfig = new SocketConfig();
        soConfig.setReuseAddress(true);
        config.setSocketConfig(soConfig);
        config.setPort(portNumber);
        config.setHostname(hostIp);
        if (useSSL) {
            try {
                final InputStream stream = new FileInputStream(keystore);
                config.setKeyStore(stream);
                config.setKeyStorePassword(storepass);
            } catch (final FileNotFoundException e) {
                LOGGER.error("Keystore {} not found on filesystem", keystore);
            }
        }
        server = new SocketIOServer(config);

        server.addEventListener("setFeedback", Feedback.class, new DataListener<Feedback>() {
            @Override
            public void onData(final SocketIOClient client, final Feedback data, final AckRequest ackSender) {
                final User u = userService.getUser2SocketId(client.getSessionId());
                final String sessionKey = userService.getSessionForUser(u.getUsername());
                LOGGER.debug("Feedback recieved: {}", new Object[] { u, sessionKey, data.getValue() });
                if (null != sessionKey) {
                    feedbackService.saveFeedback(sessionKey, data.getValue(), u);
                }
            }
        });

        server.addEventListener("setSession", Session.class, new DataListener<Session>() {
            @Override
            public void onData(final SocketIOClient client, final Session session, final AckRequest ackSender) {
                final User u = userService.getUser2SocketId(client.getSessionId());
                if (null == u) {
                    LOGGER.info("Client {} requested to join session but is not mapped to a user",
                            client.getSessionId());

                    return;
                }
                final String oldSessionKey = userService.getSessionForUser(u.getUsername());
                if (session.getKeyword() == oldSessionKey) {
                    return;
                }

                if (null != sessionService.joinSession(session.getKeyword(), client.getSessionId())) {
                    /* active user count has to be sent to the client since the broadcast is
                     * not always sent as long as the polling solution is active simultaneously */
                    reportActiveUserCountForSession(session.getKeyword());
                    reportSessionDataToClient(session.getKeyword(), u, client);
                }
                if (null != oldSessionKey) {
                    reportActiveUserCountForSession(oldSessionKey);
                }
            }
        });

        server.addEventListener("readInterposedQuestion",
                de.thm.arsnova.entities.transport.InterposedQuestion.class,
                new DataListener<de.thm.arsnova.entities.transport.InterposedQuestion>() {
                    @Override
                    public void onData(SocketIOClient client,
                            de.thm.arsnova.entities.transport.InterposedQuestion question, AckRequest ackRequest) {
                        final User user = userService.getUser2SocketId(client.getSessionId());
                        try {
                            questionService.readInterposedQuestionInternal(question.getId(), user);
                        } catch (NotFoundException | UnauthorizedException e) {
                            LOGGER.error("Loading of question {} failed for user {} with exception {}",
                                    question.getId(), user, e.getMessage());
                        }
                    }
                });

        server.addEventListener("setLearningProgressType", LearningProgressType.class,
                new DataListener<LearningProgressType>() {
                    @Override
                    public void onData(SocketIOClient client, LearningProgressType progressType, AckRequest ack) {
                        final User user = userService.getUser2SocketId(client.getSessionId());
                        final de.thm.arsnova.entities.Session session = sessionService
                                .getSessionInternal(progressType.getSessionKeyword(), user);
                        if (session.isCreator(user)) {
                            session.setLearningProgressType(progressType.getLearningProgressType());
                            sessionService.updateSessionInternal(session, user);
                            broadcastInSession(session.getKeyword(), "learningProgressType",
                                    progressType.getLearningProgressType());
                        }
                    }
                });

        server.addConnectListener(new ConnectListener() {
            @Override
            public void onConnect(final SocketIOClient client) {
            }
        });

        server.addDisconnectListener(new DisconnectListener() {
            @Override
            public void onDisconnect(final SocketIOClient client) {
                if (userService == null || client.getSessionId() == null
                        || userService.getUser2SocketId(client.getSessionId()) == null) {
                    return;
                }
                final String username = userService.getUser2SocketId(client.getSessionId()).getUsername();
                final String sessionKey = userService.getSessionForUser(username);
                userService.removeUserFromSessionBySocketId(client.getSessionId());
                userService.removeUser2SocketId(client.getSessionId());
                if (null != sessionKey) {
                    /* user disconnected before joining a session */
                    reportActiveUserCountForSession(sessionKey);
                }
            }
        });

        server.start();
    }

    public void stopServer() {
        LOGGER.trace("In stopServer method of class: {}", getClass().getName());
        try {
            for (final SocketIOClient client : server.getAllClients()) {
                client.disconnect();
            }
        } catch (final Exception e) {
            /* If exceptions are not caught they could prevent the Socket.IO server from shutting down. */
            LOGGER.error("Exception caught on Socket.IO shutdown: {}", e.getStackTrace());
        }
        server.stop();

    }

    @Override
    public int getPortNumber() {
        return portNumber;
    }

    @Required
    public void setPortNumber(final int portNumber) {
        this.portNumber = portNumber;
    }

    public String getHostIp() {
        return hostIp;
    }

    public void setHostIp(final String hostIp) {
        this.hostIp = hostIp;
    }

    public String getStorepass() {
        return storepass;
    }

    @Required
    public void setStorepass(final String storepass) {
        this.storepass = storepass;
    }

    public String getKeystore() {
        return keystore;
    }

    @Required
    public void setKeystore(final String keystore) {
        this.keystore = keystore;
    }

    @Override
    public boolean isUseSSL() {
        return useSSL;
    }

    @Required
    public void setUseSSL(final boolean useSSL) {
        this.useSSL = useSSL;
    }

    public void reportDeletedFeedback(final User user, final Set<de.thm.arsnova.entities.Session> arsSessions) {
        final List<String> keywords = new ArrayList<String>();
        for (final de.thm.arsnova.entities.Session session : arsSessions) {
            keywords.add(session.getKeyword());
        }
        this.sendToUser(user, "feedbackReset", keywords);
    }

    private List<UUID> findConnectionIdForUser(final User user) {
        final List<UUID> result = new ArrayList<UUID>();
        for (final Entry<UUID, User> e : userService.socketId2User()) {
            final UUID someUsersConnectionId = e.getKey();
            final User someUser = e.getValue();
            if (someUser.equals(user)) {
                result.add(someUsersConnectionId);
            }
        }
        return result;
    }

    private void sendToUser(final User user, final String event, Object data) {
        final List<UUID> connectionIds = findConnectionIdForUser(user);
        if (connectionIds.isEmpty()) {
            return;
        }
        for (final SocketIOClient client : server.getAllClients()) {
            if (connectionIds.contains(client.getSessionId())) {
                client.sendEvent(event, data);
            }
        }
    }

    /**
     * Currently only sends the feedback data to the client. Should be used for all
     * relevant Socket.IO data, the client needs to know after joining a session.
     *
     * @param sessionKey
     * @param user
     * @param client
     */
    public void reportSessionDataToClient(final String sessionKey, final User user, final SocketIOClient client) {
        final de.thm.arsnova.entities.Session session = sessionService.getSessionInternal(sessionKey, user);
        client.sendEvent("unansweredLecturerQuestions",
                questionService.getUnAnsweredLectureQuestionIds(sessionKey, user));
        client.sendEvent("unansweredPreparationQuestions",
                questionService.getUnAnsweredPreparationQuestionIds(sessionKey, user));
        client.sendEvent("countLectureQuestionAnswers",
                questionService.countLectureQuestionAnswersInternal(sessionKey));
        client.sendEvent("countPreparationQuestionAnswers",
                questionService.countPreparationQuestionAnswersInternal(sessionKey));
        client.sendEvent("activeUserCountData", sessionService.activeUsers(sessionKey));
        client.sendEvent("learningProgressType", session.getLearningProgressType());
        final de.thm.arsnova.entities.Feedback fb = feedbackService.getFeedback(sessionKey);
        client.sendEvent("feedbackData", fb.getValues());
        try {
            final long averageFeedback = feedbackService.getAverageFeedbackRounded(sessionKey);
            client.sendEvent("feedbackDataRoundedAverage", averageFeedback);
        } catch (final NoContentException e) {
            final Object object = null; // can't directly use "null".
            client.sendEvent("feedbackDataRoundedAverage", object);
        }
    }

    public void reportUpdatedFeedbackForSession(final de.thm.arsnova.entities.Session session) {
        final de.thm.arsnova.entities.Feedback fb = feedbackService.getFeedback(session.getKeyword());
        broadcastInSession(session.getKeyword(), "feedbackData", fb.getValues());
        try {
            final long averageFeedback = feedbackService.getAverageFeedbackRounded(session.getKeyword());
            broadcastInSession(session.getKeyword(), "feedbackDataRoundedAverage", averageFeedback);
        } catch (final NoContentException e) {
            broadcastInSession(session.getKeyword(), "feedbackDataRoundedAverage", null);
        }
    }

    public void reportFeedbackForUserInSession(final de.thm.arsnova.entities.Session session, final User user) {
        final de.thm.arsnova.entities.Feedback fb = feedbackService.getFeedback(session.getKeyword());
        Long averageFeedback;
        try {
            averageFeedback = feedbackService.getAverageFeedbackRounded(session.getKeyword());
        } catch (final NoContentException e) {
            averageFeedback = null;
        }
        final List<UUID> connectionIds = findConnectionIdForUser(user);
        if (connectionIds.isEmpty()) {
            return;
        }

        for (final SocketIOClient client : server.getAllClients()) {
            if (connectionIds.contains(client.getSessionId())) {
                client.sendEvent("feedbackData", fb.getValues());
                client.sendEvent("feedbackDataRoundedAverage", averageFeedback);
            }
        }
    }

    public void reportActiveUserCountForSession(final String sessionKey) {
        /* This check is needed as long as the HTTP polling solution is active simultaneously. */
        final int count = userService.getUsersInSession(sessionKey).size();

        broadcastInSession(sessionKey, "activeUserCountData", count);
    }

    public void reportAnswersToLecturerQuestionAvailable(final de.thm.arsnova.entities.Session session,
            final Question lecturerQuestion) {
        broadcastInSession(session.getKeyword(), "answersToLecQuestionAvail", lecturerQuestion.get_id());
    }

    public void reportAudienceQuestionAvailable(final de.thm.arsnova.entities.Session session,
            final InterposedQuestion audienceQuestion) {
        /* TODO role handling implementation, send this only to users with role lecturer */
        broadcastInSession(session.getKeyword(), "audQuestionAvail", audienceQuestion.get_id());
    }

    public void reportLecturerQuestionAvailable(final de.thm.arsnova.entities.Session session,
            final Question lecturerQuestion) {
        /* TODO role handling implementation, send this only to users with role audience */
        broadcastInSession(session.getKeyword(), "lecQuestionAvail", lecturerQuestion.get_id()); // deprecated!
        broadcastInSession(session.getKeyword(), "lecturerQuestionAvailable", lecturerQuestion);
    }

    public void reportSessionStatus(final String sessionKey, final boolean active) {
        broadcastInSession(sessionKey, "setSessionActive", active);
    }

    public void broadcastInSession(final String sessionKey, final String eventName, final Object data) {
        /**
         * collect a list of users which are in the current session iterate over
         * all connected clients and if send feedback, if user is in current
         * session
         */
        final Set<User> users = userService.getUsersInSession(sessionKey);

        for (final SocketIOClient c : server.getAllClients()) {
            final User u = userService.getUser2SocketId(c.getSessionId());
            if (u != null && users.contains(u)) {
                c.sendEvent(eventName, data);
            }
        }
    }

    @Override
    public void visit(NewQuestionEvent event) {
        this.reportLecturerQuestionAvailable(event.getSession(), new Question(event.getQuestion()));
    }

    @Override
    public void visit(NewInterposedQuestionEvent event) {
        this.reportAudienceQuestionAvailable(event.getSession(), event.getQuestion());
    }

    @Async
    @Override
    public void visit(NewAnswerEvent event) {
        final String sessionKey = event.getSession().getKeyword();
        this.reportAnswersToLecturerQuestionAvailable(event.getSession(), new Question(event.getQuestion()));
        // TODO: These events are currently unused. Uncomment once the client does something with the data.
        //broadcastInSession(sessionKey, "countQuestionAnswersByQuestion", questionService.getAnswerAndAbstentionCountByQuestion(event.getQuestion().get_id()));
        //broadcastInSession(sessionKey, "countLectureQuestionAnswers", questionService.countLectureQuestionAnswersInternal(sessionKey));
        //broadcastInSession(sessionKey, "countPreparationQuestionAnswers", questionService.countPreparationQuestionAnswersInternal(sessionKey));

        // Update the unanswered count for the question variant that was answered.
        final de.thm.arsnova.entities.Question question = event.getQuestion();
        if (question.getQuestionVariant().equals("lecture")) {
            sendToUser(event.getUser(), "unansweredLecturerQuestions",
                    questionService.getUnAnsweredLectureQuestionIds(sessionKey, event.getUser()));
        } else if (question.getQuestionVariant().equals("preparation")) {
            sendToUser(event.getUser(), "unansweredPreparationQuestions",
                    questionService.getUnAnsweredPreparationQuestionIds(sessionKey, event.getUser()));
        }
    }

    @Async
    @Override
    public void visit(DeleteAnswerEvent event) {
        final String sessionKey = event.getSession().getKeyword();
        this.reportAnswersToLecturerQuestionAvailable(event.getSession(), new Question(event.getQuestion()));
        // We do not know which user's answer was deleted, so we can't update his 'unanswered' list of questions...
        broadcastInSession(sessionKey, "countLectureQuestionAnswers",
                questionService.countLectureQuestionAnswersInternal(sessionKey));
        broadcastInSession(sessionKey, "countPreparationQuestionAnswers",
                questionService.countPreparationQuestionAnswersInternal(sessionKey));
    }

    @Async
    @Override
    public void visit(PiRoundDelayedStartEvent event) {
        final String sessionKey = event.getSession().getKeyword();

        broadcastInSession(sessionKey, "startDelayedPiRound", event.getPiRoundInformations());
    }

    @Async
    @Override
    public void visit(PiRoundEndEvent event) {
        final String sessionKey = event.getSession().getKeyword();

        broadcastInSession(sessionKey, "endPiRound", event.getQuestionId());
    }

    @Override
    public void visit(DeleteQuestionEvent deleteQuestionEvent) {
        // TODO Auto-generated method stub

    }

    @Override
    public void visit(DeleteAllQuestionsAnswersEvent deleteAllAnswersEvent) {
        // TODO Auto-generated method stub

    }

    @Override
    public void visit(DeleteAllPreparationAnswersEvent deleteAllPreparationAnswersEvent) {
        // TODO Auto-generated method stub

    }

    @Override
    public void visit(DeleteAllLectureAnswersEvent deleteAllLectureAnswersEvent) {
        // TODO Auto-generated method stub

    }

    @Override
    public void visit(DeleteInterposedQuestionEvent deleteInterposedQuestionEvent) {
        // TODO Auto-generated method stub

    }

    @Override
    public void visit(NewFeedbackEvent event) {
        this.reportUpdatedFeedbackForSession(event.getSession());
    }

    @Override
    public void visit(DeleteFeedbackForSessionsEvent event) {
        this.reportDeletedFeedback(event.getUser(), event.getSessions());

    }

    @Override
    public void visit(StatusSessionEvent event) {
        this.reportSessionStatus(event.getSession().getKeyword(), event.getSession().isActive());
    }

    @Override
    public void visit(ChangeLearningProgress event) {
        broadcastInSession(event.getSession().getKeyword(), "learningProgressChange", null);
    }
}