![JAR search and dependency download from the Maven repository](/logo.png)
org.xwiki.netflux.internal.NetfluxEndpoint Maven / Gradle / Ivy
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.netflux.internal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.websocket.CloseReason;
import javax.websocket.Endpoint;
import javax.websocket.EndpointConfig;
import javax.websocket.Session;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.websocket.AbstractPartialStringMessageHandler;
import org.xwiki.websocket.EndpointComponent;
/**
* The Netflux WebSocket end-point.
*
* @version $Id: f50312175c67fa237c4d7a4a62b2f810353fd9fd $
* @since 13.9RC1
*/
@Component
@Singleton
@Named("netflux")
public class NetfluxEndpoint extends Endpoint implements EndpointComponent
{
// The client side keeps the connection alive by sending a PING message from time to time, using a timer
// (setTimeout). The browsers are slowing down timers used by inactive tabs / windows (that don't have
// the user focus). This is called timer throttling and can go up to 1 minute, which means inactive browser tabs
// won't be able to send PING messages more often than every minute. For this reason, we set the session idle
// timeout a little bit higher than the timer throttling value to make sure the WebSocket connection is not closed
// in background tabs.
// See https://developer.chrome.com/blog/timer-throttling-in-chrome-88/
private static final long TIMEOUT_MILLISECONDS = 65000;
private static final String NETFLUX_USER = "netflux.user";
private static final String COMMAND_LEAVE = "LEAVE";
private static final String COMMAND_JOIN = "JOIN";
private static final String ERROR_INVALID = "EINVAL";
private static final String ERROR_NO_ENTITY = "ENOENT";
private final Object bigLock = new Object();
private final Map users = new HashMap<>();
@Inject
private Logger logger;
@Inject
private IdGenerator idGenerator;
@Inject
private ChannelStore channels;
@Inject
private MessageDispatcher dispatcher;
@Override
public void onOpen(Session session, EndpointConfig config)
{
synchronized (this.bigLock) {
// Close the session if we don't receive any message from the user in TIMEOUT_MILLISECONDS.
session.setMaxIdleTimeout(TIMEOUT_MILLISECONDS);
User user = getOrRegisterUser(session);
// Send the IDENT message.
String identMessage = this.dispatcher.buildDefault("", "IDENT", user.getName(), null);
if (!sendMessage(user, identMessage)) {
return;
}
session.addMessageHandler(new AbstractPartialStringMessageHandler()
{
@Override
public void onMessage(String message)
{
handleMessage(session, message);
}
});
}
}
@Override
public void onClose(Session session, CloseReason closeReason)
{
synchronized (this.bigLock) {
wsDisconnect(session, closeReason);
}
}
@Override
public void onError(Session session, Throwable e)
{
this.logger.debug("Session closed with error.", e);
onClose(session,
new CloseReason(CloseReason.CloseCodes.CLOSED_ABNORMALLY, ExceptionUtils.getRootCauseMessage(e)));
}
private void handleMessage(Session session, String message)
{
SendJob sendJob;
synchronized (this.bigLock) {
onMessage(session, message);
sendJob = getSendJob();
}
while (sendJob != null) {
for (String msg : sendJob.getMessages()) {
if (!sendJob.getUser().isConnected()) {
break;
}
if (!sendMessage(sendJob.getUser(), msg)) {
return;
}
}
sendJob = getSendJob();
}
}
private void wsDisconnect(Session session, CloseReason closeReason)
{
synchronized (this.bigLock) {
User user = getOrRegisterUser(session);
this.logger.debug("Last message from [{}] received [{}ms] ago. Session idle timeout is [{}].",
user.getName(), System.currentTimeMillis() - user.getTimeOfLastMessage(), session.getMaxIdleTimeout());
this.logger.debug("Disconnect [{}] because [{}] ([{}])", user.getName(), closeReason.getReasonPhrase(),
closeReason.getCloseCode());
this.users.remove(user.getName());
user.setConnected(false);
// We copy the set of channels because we're modifying it while iterating over it.
new LinkedList(user.getChannels())
.forEach(channel -> leaveChannel(user, channel, "Quit: [ wsDisconnect() ]"));
}
}
private User getOrRegisterUser(Session session)
{
User user = (User) session.getUserProperties().get(NETFLUX_USER);
if (user == null) {
// Register the user.
String userId = this.idGenerator.generateUserId();
user = new User(session, userId);
this.users.put(userId, user);
session.getUserProperties().put(NETFLUX_USER, user);
this.logger.debug("Registered [{}]", userId);
}
return user;
}
private void onMessage(Session session, String message)
{
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy