/*
* Copyright 2000-2006 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrains.communicator.p2p;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationDisplayType;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.util.Pair;
import com.intellij.util.ArrayUtil;
import com.intellij.util.SmartList;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import icons.IdetalkCoreIcons;
import jetbrains.communicator.core.*;
import jetbrains.communicator.core.commands.NamedUserCommand;
import jetbrains.communicator.core.dispatcher.AsyncMessageDispatcher;
import jetbrains.communicator.core.dispatcher.Message;
import jetbrains.communicator.core.transport.Transport;
import jetbrains.communicator.core.transport.WasAddedXmlMessage;
import jetbrains.communicator.core.transport.XmlMessage;
import jetbrains.communicator.core.users.*;
import jetbrains.communicator.ide.IDEFacade;
import jetbrains.communicator.ide.NullProgressIndicator;
import jetbrains.communicator.ide.ProgressIndicator;
import jetbrains.communicator.p2p.commands.AddOnlineUserP2PCommand;
import jetbrains.communicator.p2p.commands.SendXmlMessageP2PCommand;
import jetbrains.communicator.util.StringUtil;
import jetbrains.communicator.util.UIUtil;
import jetbrains.communicator.util.WaitFor;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.ide.BuiltInServerManager;
import org.jetbrains.ide.CustomPortServerManager;
import org.jetbrains.io.CustomPortServerManagerBase;
import org.picocontainer.Disposable;
import org.picocontainer.MutablePicoContainer;
import javax.swing.*;
import java.net.InetAddress;
import java.util.*;
/**
* @author Kir Maximov
*/
public class P2PTransport implements Transport, UserMonitorClient, Disposable {
private static final Logger LOG = Logger.getLogger(P2PTransport.class);
static final String CODE = "P2P";
static final int XML_RPC_PORT = MulticastPingThread.MULTICAST_PORT + 1;
private final UserMonitorThread myUserMonitorThread;
private final Object myLock = new Object();
private final Map<User, OnlineUserInfo> myUserToInfo = new THashMap<>();
private final Map<User, OnlineUserInfo> myUserToInfoNew = new THashMap<>();
private final Collection<User> myOnlineUsers = new THashSet<>();
private final EventBroadcaster myEventBroadcaster;
private final IDEtalkListener myUserAddedCallbackListener;
private final AsyncMessageDispatcher myAsyncMessageDispatcher;
private final UserModel myUserModel;
private UserPresence myOwnPresence;
public P2PTransport(AsyncMessageDispatcher asyncMessageDispatcher, UserModel userModel) {
this(asyncMessageDispatcher, userModel, UserMonitorThread.WAIT_USER_RESPONSES_TIMEOUT);
}
public P2PTransport(AsyncMessageDispatcher asyncMessageDispatcher, UserModel userModel, long waitUserResponsesTimeout) {
myEventBroadcaster = userModel.getBroadcaster();
myAsyncMessageDispatcher = asyncMessageDispatcher;
myUserModel = userModel;
myUserAddedCallbackListener = new TransportUserListener(this) {
@Override
protected void processAfterChange(UserEvent event) {
event.accept(new EventVisitor() {
@Override
public void visitUserAdded(UserEvent.Added event) {
super.visitUserAdded(event);
sendUserAddedCallback(event.getUser());
}
});
}
};
myOwnPresence = new UserPresence(true);
myUserMonitorThread = new UserMonitorThread(this, waitUserResponsesTimeout);
Map<String, Object> handlers = CustomPortServerManager.EP_NAME.findExtension(P2PCustomPortServerManager.class).handlers;
for (P2PCommand command : new P2PCommand[]{
new SendXmlMessageP2PCommand(myEventBroadcaster, this),
new AddOnlineUserP2PCommand(myUserMonitorThread),
}) {
handlers.put(command.getXmlRpcId(), command);
}
startup();
Runtime.getRuntime().addShutdownHook(new IDETalkShutdownHook());
}
class IDETalkShutdownHook extends Thread {
public IDETalkShutdownHook() {
super("IDE Talk shutdown hook");
setDaemon(true);
}
@Override
public void run() {
dispose();
}
}
@SuppressWarnings("UnusedDeclaration")
private static final class P2PCustomPortServerManager extends CustomPortServerManagerBase {
private final Map<String, Object> handlers = Collections.synchronizedMap(new THashMap<String, Object>());
@Override
public void cannotBind(Exception e, int port) {
String groupDisplayId = "IDETalk XmlRpc Server";
Notifications.Bus.register(groupDisplayId, NotificationDisplayType.STICKY_BALLOON);
new Notification(groupDisplayId, "IDETalk XmlRpc server on custom port " + port + " disabled",
"Cannot start IDETalk XmlRpc server on custom port " + port + "." +
"Please ensure that port is free (or check your firewall settings) and restart " + ApplicationNamesInfo.getInstance().getFullProductName(),
NotificationType.ERROR).notify(null);
}
@Override
public int getPort() {
return XML_RPC_PORT;
}
@Override
public boolean isAvailableExternally() {
return true;
}
@Nullable
@Override
public Map<String, Object> createXmlRpcHandlers() {
return handlers;
}
}
private void startup() {
Application application = ApplicationManager.getApplication();
if (application.isUnitTestMode()) {
doStart();
}
else {
application.executeOnPooledThread(() -> doStart());
}
}
private void doStart() {
BuiltInServerManager.getInstance().waitForStart();
myUserMonitorThread.start();
myUserMonitorThread.triggerFindNow();
new WaitFor() {
@Override
protected boolean condition() {
return myUserMonitorThread.isRunning();
}
};
myEventBroadcaster.addListener(myUserAddedCallbackListener);
}
IDEFacade getIdeFacade() {
return myAsyncMessageDispatcher.getIdeFacade();
}
@Override
public void dispose() {
try {
myEventBroadcaster.removeListener(myUserAddedCallbackListener);
myUserMonitorThread.shutdown();
}
catch (Throwable e) {
LOG.info(e);
}
myOnlineUsers.clear();
}
@Override
public void initializeProject(final String projectName, MutablePicoContainer projectLevelContainer) {
getIdeFacade().runOnPooledThread(() -> {
User[] users = findUsers(new NullProgressIndicator());
Set<User> ourUsers = new HashSet<>();
for (User user : users) {
if (Arrays.asList(user.getProjects()).contains(projectName)) {
ourUsers.add(user);
}
}
if (canAddUsers(projectName, ourUsers)) {
for (User user : ourUsers) {
if (!myUserModel.hasUser(user)) {
user.setGroup(projectName, myUserModel);
myUserModel.addUser(user);
}
}
}
});
}
boolean canAddUsers(String projectName, Collection<User> users) {
if (users.size() == 0) return false;
return hasGroup(projectName) || hasNoUsersFrom(users);
}
private boolean hasNoUsersFrom(Collection<User> users) {
for (User user : users) {
if (myUserModel.hasUser(user)) {
return false;
}
}
return true;
}
private boolean hasGroup(String projectName) {
return Arrays.asList(myUserModel.getGroups()).contains(projectName);
}
@Override
public Class<? extends NamedUserCommand> getSpecificFinderClass() {
return null;
}
@Override
public String getName() {
return CODE;
}
@Override
public User[] findUsers(ProgressIndicator progressIndicator) {
myUserMonitorThread.findNow(progressIndicator);
return myOnlineUsers.toArray(new User[myOnlineUsers.size()]);
}
@Override
public boolean isOnline() {
return myOwnPresence.isOnline();
}
@Override
public boolean hasIdeTalkClient(User user) {
return true;
}
@Override
public UserPresence getUserPresence(User user) {
boolean online;
synchronized (myLock) {
online = myOnlineUsers.contains(user);
if (online) {
return getNotNullOnlineInfo(user).getPresence();
}
}
return new UserPresence(false);
}
public InetAddress getAddress(User p2PUser) {
return getNotNullOnlineInfo(p2PUser).getAddress();
}
@Override
public boolean isSelf(User user) {
InetAddress address = getAddress(user);
return StringUtil.getMyUsername().equals(user.getName()) && NetworkUtil.isOwnAddress(address);
}
@Override
public Icon getIcon(UserPresence userPresence) {
return UIUtil.getIcon(userPresence, IdetalkCoreIcons.IdeTalk.User, IdetalkCoreIcons.IdeTalk.User_dnd);
}
public int getPort(User user) {
return getNotNullOnlineInfo(user).getPort();
}
@Override
public String[] getProjects(User user) {
return ArrayUtil.toStringArray(getNotNullOnlineInfo(user).getProjects());
}
@NotNull
private OnlineUserInfo getNotNullOnlineInfo(User user) {
OnlineUserInfo result;
synchronized (myLock) {
result = myUserToInfo.get(user);
}
if (result == null) {
result = new OnlineUserInfo(null, -1, new THashSet<>(), new UserPresence(false));
}
return result;
}
@Override
public String getAddressString(User user) {
return getAddress(user).getHostAddress() + ':' + getPort(user);
}
@Override
public void sendXmlMessage(User user, XmlMessage message) {
Message msg = SendXmlMessageP2PCommand.createNetworkMessage(message);
if (message.needsResponse()) {
myAsyncMessageDispatcher.sendNow(user, msg);
}
else {
myAsyncMessageDispatcher.sendLater(user, msg);
}
}
@Override
public UserPresence getOwnPresence() {
return myOwnPresence;
}
@Override
public void setOwnPresence(UserPresence userPresence) {
if (!myOwnPresence.isOnline() && userPresence.isOnline()) {
startup();
}
else if (myOwnPresence.isOnline() && !userPresence.isOnline()) {
dispose();
}
if (selfBecomeAvailable(userPresence)) {
notifyUsersAboutOnlineImmediately();
}
myOwnPresence = userPresence;
}
private boolean selfBecomeAvailable(UserPresence userPresence) {
return
myOwnPresence.getPresenceMode() != PresenceMode.AVAILABLE &&
userPresence.getPresenceMode() == PresenceMode.AVAILABLE;
}
private void notifyUsersAboutOnlineImmediately() {
for (User user : myUserModel.getAllUsers()) {
if (user.getTransportCode().equals(getName())) {
user.sendXmlMessage(new BecomeAvailableXmlMessage());
}
}
}
@Override
public void setOnlineUsers(@NotNull Collection<User> onlineUsers) {
removeOfflineUsersAndUpdateOldOnlineUsers(onlineUsers);
addNewOnlineUsers(onlineUsers);
synchronized (myLock) {
myUserToInfoNew.clear();
}
}
public void setAvailable(String remoteUser) {
final User user = myUserModel.findUser(remoteUser, getName());
if (user != null) {
UserPresence oldPresence = getNotNullOnlineInfo(user).getPresence();
UserPresence newPresence = new UserPresence(true);
myEventBroadcaster.doChange(new UserEvent.Updated(user, "presence", oldPresence, newPresence), new MySyncRunnable() {
@Override
protected void execute() {
OnlineUserInfo onlineInfo = getNotNullOnlineInfo(user);
onlineInfo.setPresence(new UserPresence(true));
myUserToInfo.put(user, onlineInfo);
}
});
}
}
private abstract class MySyncRunnable implements Runnable {
@Override
public final void run() {
synchronized (myLock) {
execute();
}
}
protected abstract void execute();
}
@Override
public User createUser(String remoteUsername, @NotNull OnlineUserInfo onlineUserInfo) {
synchronized (myLock) {
User user = myUserModel.createUser(remoteUsername, CODE);
myUserToInfoNew.put(user, onlineUserInfo);
return user;
}
}
void flushCurrentUsers() {
myUserMonitorThread.flushOnlineUsers();
}
UserMonitorThread getUserMonitorThread() {
return myUserMonitorThread;
}
protected void sendUserAddedCallback(User user) {
sendXmlMessage(user, new WasAddedXmlMessage());
}
@Override
public int getPort() {
return XML_RPC_PORT;
}
private void addNewOnlineUsers(@NotNull Collection<User> onlineUsers) {
List<Pair<IDEtalkEvent, Runnable>> events = new SmartList<>();
synchronized (myLock) {
for (final User user : onlineUsers) {
if (!myOnlineUsers.contains(user) && myUserToInfoNew.containsKey(user)) {
events.add(new Pair<>(new UserEvent.Online(user), new MySyncRunnable() {
@Override
protected void execute() {
myOnlineUsers.add(user);
myUserToInfo.put(user, myUserToInfoNew.get(user));
}
}));
}
}
}
dispatchEvents(events);
}
private void removeOfflineUsersAndUpdateOldOnlineUsers(@NotNull Collection onlineUsers) {
List<Pair<IDEtalkEvent, Runnable>> events = new SmartList<>();
synchronized (myLock) {
for (final User user : myOnlineUsers) {
if (!onlineUsers.contains(user)) {
// User was removed
events.add(new Pair<>(new UserEvent.Offline(user), new MySyncRunnable() {
@Override
protected void execute() {
myOnlineUsers.remove(user);
myUserToInfo.remove(user);
}
}));
}
else {
// User already exists
UserPresence oldPresence = getNotNullOnlineInfo(user).getPresence();
final OnlineUserInfo onlineUserInfo = myUserToInfoNew.get(user);
if (onlineUserInfo == null) {
return;
}
UserPresence newPresence = onlineUserInfo.getPresence();
if (!newPresence.equals(oldPresence)) {
events.add(new Pair<>(new UserEvent.Updated(user, "presence", oldPresence, newPresence), new MySyncRunnable() {
@Override
protected void execute() {
myUserToInfo.put(user, onlineUserInfo);
}
}));
}
else {
myUserToInfo.put(user, onlineUserInfo);
}
}
}
}
dispatchEvents(events);
}
private void dispatchEvents(List<Pair<IDEtalkEvent, Runnable>> events) {
for (Pair<IDEtalkEvent, Runnable> event : events) {
try {
myEventBroadcaster.doChange(event.first, event.second);
}
catch (Throwable e) {
LOG.error(e);
}
}
}
public static P2PTransport getInstance() {
return (P2PTransport) Pico.getInstance().getComponentInstanceOfType(P2PTransport.class);
}
}