io.kodokojo.endpoint.WebSocketEntryPoint.java Source code

Java tutorial

Introduction

Here is the source code for io.kodokojo.endpoint.WebSocketEntryPoint.java

Source

/**
 * Kodo Kojo - Software factory done right
 * Copyright  2016 Kodo Kojo (infos@kodokojo.io)
 *
 * This program 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.
 *
 * This program 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 io.kodokojo.endpoint;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
import io.kodokojo.Launcher;
import io.kodokojo.model.BrickState;
import io.kodokojo.brick.BrickStateMsgDispatcher;
import io.kodokojo.brick.BrickStateMsgListener;
import io.kodokojo.brick.BrickUrlFactory;
import io.kodokojo.endpoint.dto.WebSocketMessage;
import io.kodokojo.endpoint.dto.WebSocketMessageGsonAdapter;
import io.kodokojo.model.ProjectConfiguration;
import io.kodokojo.model.User;
import io.kodokojo.service.store.ProjectStore;
import io.kodokojo.service.store.UserStore;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

//  WebSocket colse event code https://developer.mozilla.org/fr/docs/Web/API/CloseEvent
@WebSocket
public class WebSocketEntryPoint implements BrickStateMsgListener {

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

    public static final long USER_VALIDATION_TIMEOUT = 10000;

    private final Map<Session, Long> sessions;

    private final Map<String, UserSession> userConnectedSession;

    private final UserStore userStore;

    private final ProjectStore projectStore;

    private final BrickUrlFactory brickUrlFactory;

    private final ThreadLocal<Gson> localGson = new ThreadLocal<Gson>() {
        @Override
        protected Gson initialValue() {
            GsonBuilder builder = new GsonBuilder();
            builder.registerTypeAdapter(WebSocketMessage.class, new WebSocketMessageGsonAdapter());
            return builder.create();
        }
    };

    //  WebSocket is built by Spark but we are not able to get the instance :/ .
    //  See : https://github.com/perwendel/spark/pull/383
    public WebSocketEntryPoint() {
        super();
        sessions = new ConcurrentHashMap<>();
        userConnectedSession = new ConcurrentHashMap<>();
        userStore = Launcher.INJECTOR.getInstance(UserStore.class);
        projectStore = Launcher.INJECTOR.getInstance(ProjectStore.class);
        brickUrlFactory = Launcher.INJECTOR.getInstance(BrickUrlFactory.class);
        BrickStateMsgDispatcher msgDispatcher = Launcher.INJECTOR.getInstance(BrickStateMsgDispatcher.class);
        msgDispatcher.addListener(this);
        LOGGER.info("Create a new WebSocketEntryPoint : {}", Thread.currentThread().getStackTrace());

    }

    @OnWebSocketConnect
    public void connected(Session session) {
        LOGGER.info("Create a new session to {}.", session.getRemoteAddress().getHostString());
        sessions.put(session, System.currentTimeMillis());
    }

    @OnWebSocketMessage
    public void message(Session session, String message) throws IOException {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Receive following message: {}", message);
        }
        LOGGER.info("Receive following message: {}", message);
        UserSession userSession = sessionIsValidated(session);
        if (userSession == null) {
            Long connectDate = sessions.get(session);
            long delta = (connectDate + USER_VALIDATION_TIMEOUT) - System.currentTimeMillis();
            if (delta < 0) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Session of user wait for to many times....");
                }
                session.close(); // To late to connect.
            } else {
                Gson gson = localGson.get();
                WebSocketMessage webSocketMessage = gson.fromJson(message, WebSocketMessage.class);
                JsonObject data = null;
                if ("user".equals(webSocketMessage.getEntity())
                        && "authentication".equals(webSocketMessage.getAction())
                        && webSocketMessage.getData().has("authorization")) {
                    data = webSocketMessage.getData();
                    String encodedAutorization = data.getAsJsonPrimitive("authorization").getAsString();
                    if (encodedAutorization.startsWith("Basic ")) {
                        String encodedCredentials = encodedAutorization.substring("Basic ".length());
                        String decoded = new String(Base64.getDecoder().decode(encodedCredentials));
                        String[] credentials = decoded.split(":");
                        if (credentials.length != 2) {
                            sessions.remove(session);
                            session.close(1008, "Authorization value in data mal formatted");
                        } else {
                            User user = userStore.getUserByUsername(credentials[0]);
                            if (user == null) {
                                sessions.remove(session);
                                session.close(4401, "Invalid credentials for user '" + credentials[0] + "'.");
                            } else {
                                if (user.getPassword().equals(credentials[1])) {
                                    userConnectedSession.put(user.getIdentifier(), new UserSession(session, user));
                                    sessions.remove(session);
                                    JsonObject dataValidate = new JsonObject();
                                    dataValidate.addProperty("message", "success");
                                    dataValidate.addProperty("identifier", user.getIdentifier());
                                    WebSocketMessage response = new WebSocketMessage("user", "authentication",
                                            dataValidate);
                                    String responseStr = gson.toJson(response);
                                    session.getRemote().sendString(responseStr);
                                    if (LOGGER.isDebugEnabled()) {
                                        LOGGER.debug("Send following message to user {} : {}", user.getUsername(),
                                                responseStr);
                                    }
                                    LOGGER.info("Send welcome message to user {} : {}", user.getUsername(),
                                            responseStr);
                                } else {
                                    sessions.remove(session);
                                    session.close(4401, "Invalid credentials.");

                                }
                            }
                        }
                    } else {
                        sessions.remove(session);
                        session.close(1008,
                                "Authentication value in data attribute mal formatted : " + data.toString());
                    }
                } else {
                    sessions.remove(session);
                    session.close();
                }
            }
        } else {
            userSession.setLastActivityDate(System.currentTimeMillis());
            LOGGER.debug("Receive following message from user '{}'.", userSession.getUser().getName());
        }
    }

    @OnWebSocketClose
    public void closed(Session session, int statusCode, String reason) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Connection closed for reason '{}' with status code {}.", reason, statusCode);
        }
        LOGGER.info("Connection closed for reason '{}' with status code {}.", reason, statusCode);
        sessions.remove(session);
        String identifier = null;
        Iterator<Map.Entry<String, UserSession>> iterator = userConnectedSession.entrySet().iterator();
        while (iterator.hasNext() && identifier == null) {
            Map.Entry<String, UserSession> sessionEntry = iterator.next();
            if (sessionEntry.getValue().getSession().equals(session)) {
                identifier = sessionEntry.getKey();
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Connection closed attach to user {}.",
                            sessionEntry.getValue().getUser().getUsername());
                }
            }
        }
        if (identifier != null) {
            userConnectedSession.remove(identifier);
        }

    }

    @Override
    public void receive(BrickState brickState) {
        if (brickState == null) {
            throw new IllegalArgumentException("brickState must be defined.");
        }
        WebSocketMessage message = convertToWebSocketMessage(brickState);
        String projectConfigurationIdentifier = brickState.getProjectConfigurationIdentifier();
        ProjectConfiguration projectConfiguration = projectStore
                .getProjectConfigurationById(projectConfigurationIdentifier);
        Iterator<User> admins = projectConfiguration.getAdmins();
        List<String> adminIds = new ArrayList<>();
        admins.forEachRemaining(admin -> adminIds.add(admin.getIdentifier()));
        adminIds.stream().forEach(adminId -> {
            UserSession ownerSession = userConnectedSession.get(adminId);
            if (ownerSession != null) {
                sendMessageToUser(message, ownerSession);
                projectConfiguration.getUsers().forEachRemaining(user -> {
                    UserSession session = userConnectedSession.get(user.getIdentifier());
                    if (session != null && !ownerSession.getUser().getIdentifier().equals(user.getIdentifier())) {
                        sendMessageToUser(message, session);
                        LOGGER.info("Send message to {} :{}", ownerSession.getUser().getUsername(), message);
                    }
                });
            }
        });
    }

    private void sendMessageToUser(WebSocketMessage message, UserSession userSession) {
        Gson gson = localGson.get();
        String json = gson.toJson(message);
        try {
            userSession.getSession().getRemote().sendString(json);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Following message sent to user {} : {}", userSession.getUser().getUsername(), json);
            }
            LOGGER.info("Following message sent to user {} : {}", userSession.getUser().getUsername(), json);
        } catch (IOException e) {
            LOGGER.error("Unable to notify user {}.", userSession.getUser().getUsername());
        }
    }

    private WebSocketMessage convertToWebSocketMessage(BrickState brickState) {
        JsonObject data = new JsonObject();
        data.addProperty("projectConfiguration", brickState.getProjectConfigurationIdentifier());
        data.addProperty("brickType", brickState.getBrickType());
        data.addProperty("brickName", brickState.getBrickName());
        data.addProperty("state", brickState.getState().name());
        if (brickState.getState() == BrickState.State.RUNNING) {
            ProjectConfiguration projectConfiguration = projectStore
                    .getProjectConfigurationById(brickState.getProjectConfigurationIdentifier());
            data.addProperty("url", "https://" + brickUrlFactory.forgeUrl(projectConfiguration.getName(),
                    brickState.getStackName(), brickState.getBrickType(), brickState.getBrickName()));
        }
        if (brickState.getState() == BrickState.State.ONFAILURE) {
            data.addProperty("message", brickState.getMessage());
        }

        return new WebSocketMessage("brick", "updateState", data);
    }

    private UserSession sessionIsValidated(Session session) {
        assert session != null : "session must be defined";
        UserSession res = null;
        Iterator<UserSession> iterator = userConnectedSession.values().iterator();
        while (res == null && iterator.hasNext()) {
            UserSession current = iterator.next();
            if (current.getSession().equals(session)) {
                res = current;
            }
        }
        return res;
    }

    private class UserSession {

        private final Session session;

        private final User user;

        private long lastActivityDate;

        public UserSession(Session session, User user) {
            this.session = session;
            this.user = user;
            this.lastActivityDate = System.currentTimeMillis();
        }

        public Session getSession() {
            return session;
        }

        public User getUser() {
            return user;
        }

        public long getLastActivityDate() {
            return lastActivityDate;
        }

        public void setLastActivityDate(long lastActivityDate) {
            if (this.lastActivityDate < lastActivityDate)
                this.lastActivityDate = lastActivityDate;
        }
    }

}