Skip to content

Commit

Permalink
Separated endpoint class into web socket and user
Browse files Browse the repository at this point in the history
This allows for login/logout and other neat things.
  • Loading branch information
ekuiter committed Sep 9, 2018
1 parent dc19709 commit 382a796
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 124 deletions.
4 changes: 2 additions & 2 deletions client/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ const constants = {
UNDO: 'UNDO',
REDO: 'REDO',
MULTIPLE_MESSAGES: 'MULTIPLE_MESSAGES',
USER_SUBSCRIBE: 'USER_SUBSCRIBE',
USER_UNSUBSCRIBE: 'USER_UNSUBSCRIBE',
USER_JOINED: 'USER_JOINED',
USER_LEFT: 'USER_LEFT',
FEATURE_DIAGRAM_FEATURE_MODEL: 'FEATURE_DIAGRAM_FEATURE_MODEL',
FEATURE_DIAGRAM_FEATURE_ADD_BELOW: 'FEATURE_DIAGRAM_FEATURE_ADD_BELOW',
FEATURE_DIAGRAM_FEATURE_ADD_ABOVE: 'FEATURE_DIAGRAM_FEATURE_ADD_ABOVE',
Expand Down
4 changes: 2 additions & 2 deletions client/src/server/messageReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ const messageTypes = constants.server.messageTypes,
console.warn(action.error);
return state;
},
[messageTypes.USER_SUBSCRIBE](state, action) {
[messageTypes.USER_JOINED](state, action) {
return {...state, users: uniqueArrayAdd(state.users, action.user)};
},
[messageTypes.USER_UNSUBSCRIBE](state, action) {
[messageTypes.USER_LEFT](state, action) {
return {...state, users: uniqueArrayRemove(state.users, action.user)};
},
[messageTypes.FEATURE_DIAGRAM_FEATURE_MODEL](state, action) {
Expand Down
16 changes: 8 additions & 8 deletions client/src/server/messageReducer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,27 @@ describe('messageReducer', () => {
expect(console.warn).lastCalledWith('some error');
});

it('subscribes users', () => {
it('lets users join', () => {
const state = messageReducer(initialState,
{type: messageTypes.USER_SUBSCRIBE, user: 'some user'});
{type: messageTypes.USER_JOINED, user: 'some user'});
expect(state.users).toContain('some user');
});

it('does not subscribe users multiple times', () => {
it('does not let users join multiple times', () => {
let state = messageReducer(initialState,
{type: messageTypes.USER_SUBSCRIBE, user: 'some user'});
{type: messageTypes.USER_JOINED, user: 'some user'});
expect(state.users).toHaveLength(1);
state = messageReducer(state,
{type: messageTypes.USER_SUBSCRIBE, user: 'some user'});
{type: messageTypes.USER_JOINED, user: 'some user'});
expect(state.users).toHaveLength(1);
});

it('unsubscribes users', () => {
it('lets users leave', () => {
let state = messageReducer(initialState,
{type: messageTypes.USER_SUBSCRIBE, user: 'some user'});
{type: messageTypes.USER_JOINED, user: 'some user'});
expect(state.users).toContain('some user');
state = messageReducer(initialState,
{type: messageTypes.USER_UNSUBSCRIBE, user: 'some user'});
{type: messageTypes.USER_LEFT, user: 'some user'});
expect(state.users).not.toContain('some user');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
import java.util.function.Predicate;

/**
* A collaboration session consists of a set of endpoints that view and edit a feature model together.
* A collaboration session consists of a set of users that view and edit a feature model together.
*/
public class CollaborationSession {
private static CollaborationSession instance;
private StateContext stateContext;
private Set<Endpoint> endpoints = new HashSet<>();
private Set<User> users = new HashSet<>();

private CollaborationSession(StateContext stateContext) {
this.stateContext = Objects.requireNonNull(stateContext, "no state context given");
Expand All @@ -26,35 +26,35 @@ public static CollaborationSession getInstance() {
return instance == null ? instance = new CollaborationSession(StateContext.DEFAULT) : instance;
}

public void subscribe(Endpoint newEndpoint) {
if (!endpoints.add(newEndpoint))
throw new RuntimeException("endpoint already subscribed");
unicast(newEndpoint, Api.UserSubscribe::new, endpoint -> endpoint != newEndpoint);
broadcast(new Api.UserSubscribe(newEndpoint), endpoint -> endpoint != newEndpoint);
stateContext.sendInitialState(newEndpoint);
public void join(User newUser) {
if (!users.add(newUser))
throw new RuntimeException("user already joined");
unicast(newUser, Api.UserJoined::new, user -> user != newUser);
broadcast(new Api.UserJoined(newUser), user -> user != newUser);
stateContext.sendInitialState(newUser);
}

public void unsubscribe(Endpoint oldEndpoint) {
if (endpoints.remove(oldEndpoint))
broadcast(new Api.UserUnsubscribe(oldEndpoint));
public void leave(User oldUser) {
if (users.remove(oldUser))
broadcast(new Api.UserLeft(oldUser));
}

public void unicast(Endpoint targetEndpoint, Function<Endpoint, Message.IEncodable> messageFunction, Predicate<Endpoint> predicate) {
for (Endpoint endpoint : endpoints)
if (predicate.test(endpoint))
targetEndpoint.send(messageFunction.apply(endpoint));
public void unicast(User targetUser, Function<User, Message.IEncodable> messageFunction, Predicate<User> predicate) {
users.stream()
.filter(predicate)
.forEach(user -> targetUser.send(messageFunction.apply(user)));
}

public void broadcast(Message.IEncodable message, Predicate<Endpoint> predicate) {
public void broadcast(Message.IEncodable message, Predicate<User> predicate) {
Objects.requireNonNull(message, "no message given");
for (Endpoint endpoint : endpoints)
if (predicate.test(endpoint))
endpoint.send(message);
users.stream()
.filter(predicate)
.forEach(user -> user.send(message));
}

public void broadcast(Message.IEncodable message) {
Objects.requireNonNull(message, "no message given");
broadcast(message, endpoint -> true);
broadcast(message, user -> true);
}

public void broadcast(Message.IEncodable[] messages) {
Expand All @@ -63,7 +63,7 @@ public void broadcast(Message.IEncodable[] messages) {
broadcast(message);
}

public void onMessage(Endpoint endpoint, Message message) {
public void onMessage(Message message) {
Objects.requireNonNull(message, "no message given");
Message.IDecodable decodableMessage = (Message.IDecodable) message;
if (!decodableMessage.isValid(stateContext))
Expand Down
42 changes: 0 additions & 42 deletions server/src/main/java/de/ovgu/spldev/varied/EndpointManager.java

This file was deleted.

4 changes: 2 additions & 2 deletions server/src/main/java/de/ovgu/spldev/varied/StateContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public StateChangeStack getStateChangeStack() {
return stateChangeStack;
}

public void sendInitialState(Endpoint endpoint) {
endpoint.send(new Api.FeatureDiagramFeatureModel(featureModel));
public void sendInitialState(User user) {
user.send(new Api.FeatureDiagramFeatureModel(featureModel));
}
}
47 changes: 47 additions & 0 deletions server/src/main/java/de/ovgu/spldev/varied/User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package de.ovgu.spldev.varied;

import de.ovgu.spldev.varied.messaging.Message;
import me.atrox.haikunator.Haikunator;
import me.atrox.haikunator.HaikunatorBuilder;

import java.util.function.Supplier;

public class User {
private String name;
private WebSocket webSocket;
private static Haikunator haikunator = new HaikunatorBuilder().setDelimiter(" ").setTokenLength(0).build();

private static String generateName() {
Supplier<String> generator = () -> haikunator.haikunate() + " (anonymous)";
UserManager userManager = UserManager.getInstance();
String name = generator.get();
while (!userManager.isNameAvailable(name))
name = generator.get();
return name;
}

public User(WebSocket webSocket) {
this(generateName(), webSocket);
}

public User(String name, WebSocket webSocket) {
this.name = name;
this.webSocket = webSocket;
}

public void send(Message.IEncodable message) {
webSocket.send(message);
}

public String getName() {
return name;
}

public WebSocket getWebSocket() {
return webSocket;
}

public String toString() {
return getName();
}
}
66 changes: 66 additions & 0 deletions server/src/main/java/de/ovgu/spldev/varied/UserManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package de.ovgu.spldev.varied;

import de.ovgu.spldev.varied.messaging.Message;
import de.ovgu.spldev.varied.util.StringUtils;

import java.util.HashMap;

/**
* Holds a mapping from web sockets to users and manages user registration
*/
public class UserManager {
private static UserManager instance;
private HashMap<WebSocket, User> users = new HashMap<>();

private UserManager() {
}

public static UserManager getInstance() {
return instance == null ? instance = new UserManager() : instance;
}

boolean isNameAvailable(String name) {
return users.values().stream()
.map(User::getName)
.noneMatch(name::equals);
}

public void register(User newUser) {
String name = newUser.getName();
if (!StringUtils.isPresent(name))
throw new RuntimeException("no name supplied on registration");
if (!isNameAvailable(name))
throw new RuntimeException("name already registered, choose another name");
if (users.containsValue(newUser))
throw new RuntimeException("user already registered");
if (users.containsKey(newUser.getWebSocket()))
throw new RuntimeException("web socket is already logged in as another user");
users.put(newUser.getWebSocket(), newUser);

// TODO: let the user decide which collaboration session to join
CollaborationSession.getInstance().join(newUser);
}

public void register(WebSocket webSocket) {
register(new User(webSocket));
}

public void unregister(User oldUser) {
// TODO: see above
CollaborationSession.getInstance().leave(oldUser);

users.remove(oldUser.getWebSocket());
}

public void unregister(WebSocket webSocket) {
User user = users.get(webSocket);
if (user != null)
unregister(user);
}

public void onMessage(WebSocket webSocket, Message message) {
// TODO: dispatch to currect session (by asking the user object)
// also handle login/logout here
CollaborationSession.getInstance().onMessage(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,26 @@
import de.ovgu.spldev.varied.messaging.Api;
import de.ovgu.spldev.varied.messaging.Message;
import de.ovgu.spldev.varied.messaging.MessageSerializer;
import me.atrox.haikunator.Haikunator;
import me.atrox.haikunator.HaikunatorBuilder;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.EOFException;
import java.io.IOException;

/**
* An endpoint is a client that has a bidirectional connection (session) with the server.
*/
@ServerEndpoint(
value = "/websocket",
encoders = MessageSerializer.MessageEncoder.class,
decoders = MessageSerializer.MessageDecoder.class)
public class Endpoint {
protected String label;
protected Session session;

private static Haikunator haikunator = new HaikunatorBuilder().setDelimiter(" ").setTokenLength(0).build();

public static String generateLabel() {
return haikunator.haikunate() + " (anonymous)";
}
public class WebSocket {
private Session session;

@OnOpen
public void onOpen(Session session) {
EndpointManager endpointManager = EndpointManager.getInstance();
session.setMaxIdleTimeout(0);

this.session = session;
this.label = generateLabel();
while (!endpointManager.isLabelAvailable(this.label))
this.label = generateLabel();
session.setMaxIdleTimeout(0);

try {
endpointManager.register(this);
CollaborationSession.getInstance().subscribe(this);
UserManager.getInstance().register(this);
} catch (Throwable t) {
send(new Api.Error(t));
}
Expand All @@ -49,8 +31,7 @@ public void onOpen(Session session) {
@OnClose
public void onClose() {
try {
CollaborationSession.getInstance().unsubscribe(this);
EndpointManager.getInstance().unregister(this);
UserManager.getInstance().unregister(this);
} catch (Throwable t) {
send(new Api.Error(t));
}
Expand All @@ -59,7 +40,7 @@ public void onClose() {
@OnMessage
public void onMessage(Message message) {
try {
CollaborationSession.getInstance().onMessage(this, message);
UserManager.getInstance().onMessage(this, message);
} catch (Throwable t) {
send(new Api.Error(t));
}
Expand Down Expand Up @@ -90,12 +71,4 @@ public void send(Message.IEncodable message) {
throw new RuntimeException(e);
}
}

public String getLabel() {
return label;
}

public String toString() {
return getLabel();
}
}
Loading

0 comments on commit 382a796

Please sign in to comment.