/*
* 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.jabber.impl;
import com.intellij.util.ArrayUtil;
import icons.IdetalkCoreIcons;
import jetbrains.communicator.core.*;
import jetbrains.communicator.core.commands.NamedUserCommand;
import jetbrains.communicator.core.dispatcher.AsyncMessageDispatcher;
import jetbrains.communicator.core.transport.*;
import jetbrains.communicator.core.users.*;
import jetbrains.communicator.ide.IDEFacade;
import jetbrains.communicator.ide.ProgressIndicator;
import jetbrains.communicator.jabber.ConnectionListener;
import jetbrains.communicator.jabber.JabberFacade;
import jetbrains.communicator.jabber.JabberUI;
import jetbrains.communicator.jabber.JabberUserFinder;
import jetbrains.communicator.util.IgnoreList;
import jetbrains.communicator.util.UIUtil;
import org.apache.log4j.Logger;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.Nullable;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.filter.ThreadFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.util.StringUtils;
import org.picocontainer.Disposable;
import org.picocontainer.MutablePicoContainer;
import javax.swing.*;
import java.util.*;
import java.util.concurrent.Future;
import static jetbrains.communicator.core.users.UserEvent.Updated.*;
import static jetbrains.communicator.util.StringUtil.getMsg;
/**
* @author Kir
*/
public class JabberTransport implements Transport, ConnectionListener, Disposable {
@NonNls
private static final Logger LOG = Logger.getLogger(JabberTransport.class);
private static final int RESPONSE_TIMEOUT = 120*1000;
@NonNls public static final String CODE = "Jabber";
private final JabberUI myUI;
private final JabberFacade myFacade;
private final UserModel myUserModel;
private final IDEtalkListener myUserModelListener = new MyUserModelListener();
private final AsyncMessageDispatcher myDispatcher;
private RosterListener myRosterListener;
private PacketListener mySubscribeListener;
private PacketListener myMessageListener;
private final JabberUserFinder myUserFinder;
private final IDEFacade myIdeFacade;
private final String myThreadIdPrefix = StringUtils.randomString(5);
private int myCurrentThreadId;
private boolean myIgnoreUserEvents;
private PresenceMode myPresenceMode;
private final Map<User, UserPresence> myUser2Presence = new HashMap<>();
private final Set<String> myIDEtalkUsers = new HashSet<>();
private final Map<String, String> myUser2Thread = Collections.synchronizedMap(new HashMap<String, String>());
@NonNls
private static final String RESPONSE = "response";
private final IgnoreList myIgnoreList;
// negative value disables reconnect
private int myReconnectTimeout = Integer.parseInt(System.getProperty("ideTalk.reconnect", "30")) * 1000;
private Future<?> myReconnectProcess;
public JabberTransport(JabberUI UI, JabberFacade facade, UserModel userModel, AsyncMessageDispatcher messageDispatcher, JabberUserFinder userFinder) {
Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.manual);
//XMPPConnection.DEBUG_ENABLED = true;
JDOMExtension.init();
myUI = UI;
myFacade = facade;
myUserModel = userModel;
myDispatcher = messageDispatcher;
myUserFinder = userFinder;
myIdeFacade = messageDispatcher.getIdeFacade();
myIgnoreList = new IgnoreList(myIdeFacade);
myFacade.addConnectionListener(this);
getBroadcaster().addListener(myUserModelListener);
}
private EventBroadcaster getBroadcaster() {
return myUserModel.getBroadcaster();
}
@Override
public String getName() {
return CODE;
}
@Override
public void initializeProject(String projectName, MutablePicoContainer projectLevelContainer) {
myUI.initPerProject(projectLevelContainer);
myIdeFacade.runOnPooledThread(() -> myFacade.connect());
}
@Override
public User[] findUsers(ProgressIndicator progressIndicator) {
if (isOnline()) {
return myUserFinder.findUsers(progressIndicator);
}
return new User[0];
}
@Override
public Class<? extends NamedUserCommand> getSpecificFinderClass() {
return FindByJabberIdCommand.class;
}
@Override
public boolean isOnline() {
return myFacade.isConnectedAndAuthenticated();
}
@Override
public UserPresence getUserPresence(User user) {
UserPresence presence = myUser2Presence.get(user);
if (presence == null) {
presence = new UserPresence(false);
myUser2Presence.put(user, presence);
}
return presence;
}
private UserPresence _getUserPresence(User user) {
Presence presence = _getPresence(user);
if (presence != null && presence.getType() == Presence.Type.available) {
Presence.Mode mode = presence.getMode();
final PresenceMode presenceMode;
//noinspection IfStatementWithTooManyBranches
if (mode == Presence.Mode.away) {
presenceMode = PresenceMode.AWAY;
}
else if (mode == Presence.Mode.dnd) {
presenceMode = PresenceMode.DND;
}
else if (mode == Presence.Mode.xa) {
presenceMode = PresenceMode.EXTENDED_AWAY;
}
else {
presenceMode = PresenceMode.AVAILABLE;
}
return new UserPresence(presenceMode);
}
return new UserPresence(false);
}
@Override
@NonNls
public Icon getIcon(UserPresence userPresence) {
return UIUtil.getIcon(userPresence, IdetalkCoreIcons.IdeTalk.Jabber, IdetalkCoreIcons.IdeTalk.Jabber_dnd);
}
@Override
public boolean isSelf(User user) {
return myFacade.isConnectedAndAuthenticated() && getSimpleId(myFacade.getConnection().getUser()).equals(user.getName());
}
@Override
public String[] getProjects(User user) {
return ArrayUtil.EMPTY_STRING_ARRAY;
}
@Override
@Nullable
public String getAddressString(User user) {
return null;
}
@Override
public synchronized void sendXmlMessage(User user, final XmlMessage xmlMessage) {
if (!myUI.connectAndLogin(null)) {
return;
}
final String threadId = getThreadId(user);
final PacketCollector packetCollector = myFacade.getConnection().createPacketCollector(new ThreadFilter(threadId));
doSendMessage(xmlMessage, user, threadId);
if (xmlMessage.needsResponse()) {
//noinspection HardCodedStringLiteral
final Runnable responseWaiterRunnable = () -> {
try {
processResponse(xmlMessage, packetCollector);
}
finally {
packetCollector.cancel();
}
};
myIdeFacade.runOnPooledThread(responseWaiterRunnable);
}
else {
packetCollector.cancel();
}
}
String getThreadId(User user) {
String id = myUser2Thread.get(user.getName());
if (id == null) {
id = myThreadIdPrefix + myCurrentThreadId ++;
myUser2Thread.put(user.getName(), id);
}
return id;
}
@Override
public void setOwnPresence(UserPresence userPresence) {
if (isOnline() && !userPresence.isOnline()) {
myFacade.disconnect();
}
else if (!isOnline() && userPresence.isOnline()) {
myUI.connectAndLogin(null);
}
if (isOnline() && presenceModeChanged(userPresence.getPresenceMode())) {
myFacade.setOnlinePresence(userPresence);
myPresenceMode = userPresence.getPresenceMode();
}
}
@Override
public boolean hasIdeTalkClient(User user) {
return myIDEtalkUsers.contains(user.getName());
}
private boolean presenceModeChanged(PresenceMode presenceMode) {
return myPresenceMode == null || myPresenceMode != presenceMode;
}
private static void processResponse(XmlMessage xmlMessage, PacketCollector collector) {
boolean gotResponse = false;
while (!gotResponse) {
Message response = (Message) collector.nextResult(RESPONSE_TIMEOUT);
if (response == null) break;
final Collection<PacketExtension> extensions = response.getExtensions();
for (PacketExtension o : extensions) {
if (o instanceof JDOMExtension) {
JDOMExtension extension = (JDOMExtension) o;
if (RESPONSE.equals(extension.getElement().getName())) {
xmlMessage.processResponse(extension.getElement());
gotResponse = true;
break;
}
}
}
}
}
private Message doSendMessage(XmlMessage xmlMessage, User user, String threadId) {
Element element = new Element(xmlMessage.getTagName(), xmlMessage.getTagNamespace());
xmlMessage.fillRequest(element);
Message message = createBaseMessage(user, element.getText());
message.setThread(threadId);
message.addExtension(new JDOMExtension(element));
myFacade.getConnection().sendPacket(message);
return message;
}
static Message createBaseMessage(User user, String message) {
Message msg = new Message(user.getName(), Message.Type.CHAT);
msg.setBody(message);
return msg;
}
@Override
public void connected(XMPPConnection connection) {
LOG.info("Jabber connected");
if (mySubscribeListener == null) {
mySubscribeListener = new MySubscribeListener();
connection.addPacketListener(mySubscribeListener, new PacketTypeFilter(Presence.class));
}
if (myMessageListener == null) {
myMessageListener = new MyMessageListener();
connection.addPacketListener(myMessageListener, new PacketTypeFilter(Message.class));
}
}
@Override
public void authenticated() {
LOG.info("Jabber authenticated: " + myFacade.getConnection().getUser());
if (myRosterListener == null) {
myRosterListener = new MyRosterListener();
getRoster().addRosterListener(myRosterListener);
}
myUserFinder.registerForProject(myFacade.getMyAccount().getJabberId());
if (!hasJabberContacts()) {
synchronizeRoster(false);
}
}
private boolean hasJabberContacts() {
User[] users = myUserModel.getAllUsers();
for (User user : users) {
if (user.getTransportCode().equals(getName())) return true;
}
return false;
}
@Override
public void disconnected(boolean onError) {
final XMPPConnection connection = myFacade.getConnection();
LOG.info("Jabber disconnected: " + connection.getUser());
connection.removePacketListener(mySubscribeListener);
mySubscribeListener = null;
connection.removePacketListener(myMessageListener);
myMessageListener = null;
final Roster roster = connection.getRoster();
if (roster != null) {
roster.removeRosterListener(myRosterListener);
}
myRosterListener = null;
myIDEtalkUsers.clear();
myUser2Presence.clear();
myUser2Thread.clear();
if (onError && reconnectEnabledAndNotStarted()) {
LOG.warn(getMsg("jabber.server.was.disconnected", myReconnectTimeout / 1000));
myReconnectProcess = myIdeFacade.runOnPooledThread(new MyReconnectRunnable());
}
}
private boolean reconnectEnabledAndNotStarted() {
return (myReconnectProcess == null || myReconnectProcess.isDone()) && myReconnectTimeout >= 0;
}
@Override
public void dispose() {
getBroadcaster().removeListener(myUserModelListener);
myFacade.removeConnectionListener(this);
}
private void updateUserPresence(String jabberId) {
LOG.debug("Presence changed for " + jabberId);
final User user = myUserModel.findUser(getSimpleId(jabberId), getName());
if (user != null) {
updateIsIDEtalkClient(jabberId, user);
final UserPresence presence = _getUserPresence(user);
IDEtalkEvent event = createPresenceChangeEvent(user, presence);
if (event != null) {
getBroadcaster().doChange(event, () -> myUser2Presence.put(user, presence));
}
}
}
private void updateIsIDEtalkClient(String jabberId, User user) {
if (getResource(jabberId).toLowerCase().startsWith(JabberFacade.IDETALK_RESOURCE.toLowerCase())) {
myIDEtalkUsers.add(user.getName());
} else {
myIDEtalkUsers.remove(user.getName());
}
}
@Nullable
private IDEtalkEvent createPresenceChangeEvent(User user, UserPresence newPresence) {
UserPresence oldPresence = getUserPresence(user);
if (!newPresence.equals(oldPresence)) {
if (newPresence.isOnline() ^ oldPresence.isOnline()) {
return newPresence.isOnline() ? new UserEvent.Online(user) : new UserEvent.Offline(user);
}
else {
return new UserEvent.Updated(user, PRESENCE, oldPresence.getPresenceMode(), newPresence.getPresenceMode());
}
}
return null;
}
private void updateJabberUsers(boolean removeUsersNotInRoster) {
LOG.debug("Roster changed - update user model");
Set<User> currentUsers = new HashSet<>(Arrays.asList(myUserModel.getAllUsers()));
for (RosterEntry rosterEntry : getRoster().getEntries()) {
User user = addJabberUserToUserModelOrUpdateInfo(rosterEntry);
currentUsers.remove(user);
}
if (removeUsersNotInRoster) {
removeUsers(currentUsers);
}
if (LOG.isDebugEnabled()) {
LOG.debug("Roster synchronized: " +Arrays.asList(myUserModel.getAllUsers()));
}
}
private void removeUsers(Set<User> currentUsers) {
for (User user : currentUsers) {
myUserModel.removeUser(user);
}
}
private User addJabberUserToUserModelOrUpdateInfo(RosterEntry rosterEntry) {
// System.out.println("rosterEntry.getName() = " + rosterEntry.getName());
// System.out.println("rosterEntry.getUser() = " + rosterEntry.getUser());
User user = myUserModel.createUser(getSimpleId(rosterEntry.getUser()), getName());
String newGroup = getUserGroup(rosterEntry);
if (newGroup != null) {
user.setGroup(newGroup, myUserModel);
}
user.setDisplayName(rosterEntry.getName(), myUserModel);
myUserModel.addUser(user);
String jabberId = getCurrentJabberID(user, rosterEntry);
updateIsIDEtalkClient(jabberId, user);
return user;
}
private String getCurrentJabberID(User user, RosterEntry rosterEntry) {
Presence presence = _getPresence(user);
String jabberId = null;
if (presence != null) {
jabberId = presence.getFrom();
}
if (jabberId == null) jabberId = rosterEntry.getUser();
if (jabberId == null) jabberId = rosterEntry.getName();
return jabberId;
}
static String getResource(String userName) {
int lastSlash = userName.indexOf('/');
if (lastSlash != -1) {
return userName.substring(lastSlash + 1);
}
return "";
}
static String getSimpleId(String userName) {
String id = userName;
int lastSlash = id.indexOf('/');
if (lastSlash != -1) {
id = id.substring(0, lastSlash);
}
return id;
}
@Nullable
private static String getUserGroup(RosterEntry rosterEntry) {
String group = null;
for (RosterGroup rosterGroup : rosterEntry.getGroups()) {
group = rosterGroup.getName();
}
return group;
}
private Roster getRoster() {
final Roster roster = myFacade.getConnection().getRoster();
assert roster != null;
return roster;
}
public JabberFacade getFacade() {
return myFacade;
}
boolean isUserInMyContactListAndActive(String userName) {
User user = myUserModel.findUser(getSimpleId(userName), getName());
return user != null && user.isOnline();
}
@Nullable
private Presence _getPresence(User user) {
if (!isOnline()) return null;
return getRoster().getPresence(user.getName());
}
private User self() {
return myUserModel.createUser(myFacade.getMyAccount().getJabberId(), getName());
}
public static JabberTransport getInstance() {
return (JabberTransport) Pico.getInstance().getComponentInstanceOfType(JabberTransport.class);
}
public void synchronizeRoster(boolean removeUsersNotInRoster) {
updateJabberUsers(removeUsersNotInRoster);
}
public void runIngnoringUserEvents(Runnable runnable) {
try {
myIgnoreUserEvents = true;
runnable.run();
} finally {
myIgnoreUserEvents = false;
}
}
/** -1 disables reconnect */
public void setReconnectTimeout(int milliseconds) {
myReconnectTimeout = milliseconds;
}
private class MyRosterListener implements RosterListener {
@Override
public void entriesAdded(Collection addresses) {
updateJabberUsers(false);
}
@Override
public void entriesUpdated(Collection addresses) {
updateJabberUsers(false);
}
@Override
public void entriesDeleted(Collection addresses) {
updateJabberUsers(false);
}
@Override
public void presenceChanged(final String string) {
updateUserPresence(string);
}
}
@SuppressWarnings({"RefusedBequest"})
private class MyUserModelListener extends TransportUserListener {
MyUserModelListener() {
super(JabberTransport.this);
}
@Override
protected void processBeforeChange(UserEvent event) {
super.processBeforeChange(event);
event.accept(new EventVisitor() {
@Override
public void visitUserAdded(UserEvent.Added event) {
event.getUser().setCanAccessMyFiles(false, myUserModel);
}
});
}
@Override
protected void processAfterChange(UserEvent event) {
if (myIgnoreUserEvents) return;
event.accept(new EventVisitor() {
@Override public void visitUserRemoved(UserEvent.Removed event) {
synchronizeWithJabberIfPossible(event);
}
@Override public void visitUserUpdated(UserEvent.Updated event) {
if (GROUP.equals(event.getPropertyName()) ||
DISPLAY_NAME.equals(event.getPropertyName())) {
synchronizeWithJabberIfPossible(event);
}
}
});
}
private void synchronizeWithJabberIfPossible(UserEvent event) {
if (event.getUser().getTransportCode().equals(getName()) &&
myFacade.isConnectedAndAuthenticated()) {
myDispatcher.sendNow(self(), new JabberSyncUserMessage(event));
}
}
}
private class MySubscribeListener implements PacketListener {
@Override
public void processPacket(Packet packet) {
final Presence presence = ((Presence) packet);
if (presence.getType() != Presence.Type.subscribe) return;
LOG.info("Subscribe request from " + presence.getFrom());
if (myIgnoreList.isIgnored(presence.getFrom())) {
LOG.info(presence.getFrom() + " in ignore list");
return;
}
if (isUserInMyContactListAndActive(presence.getFrom()) || Pico.isUnitTest()) {
acceptSubscription(presence, true);
return;
}
UIUtil.invokeLater(() -> acceptSubscription(presence, myUI.shouldAcceptSubscriptionRequest(presence)));
}
private void acceptSubscription(final Presence presence, boolean subscribe) {
if (!isOnline()) return;
myFacade.changeSubscription(presence.getFrom(), subscribe);
if (subscribe) {
String from = getSimpleId(presence.getFrom());
LOG.info("Add " + from + " to the roster");
try {
getRoster().createEntry(from, from, new String[]{UserModel.DEFAULT_GROUP});
} catch (XMPPException e) {
LOG.warn(e);
}
}
}
}
private class MyMessageListener implements PacketListener {
@Override
public void processPacket(Packet packet) {
try {
doProcessPacket(packet);
}
catch(Throwable e) {
LOG.error(e.getMessage(), e);
}
}
private void doProcessPacket(Packet packet) {
final Message message = ((Message) packet);
if (message.getType() == Message.Type.ERROR) {
UIUtil.invokeLater(() -> {
String from = (message.getFrom() != null) ? getMsg("from.0.lf", message.getFrom()) : "";
LOG.warn(getMsg("jabber.error.text", from,
(message.getError() == null ? "N/A" : message.getError().toString())));
});
return;
}
if (myIgnoreList.isIgnored(packet.getFrom())) {
return;
}
Element element = null;
for (PacketExtension o : message.getExtensions()) {
if (o instanceof JDOMExtension) {
element = ((JDOMExtension) o).getElement();
}
}
if (element != null && !RESPONSE.equals(element.getName())) {
processAndSendResponse(element, message);
}
else if (element == null && message.getBody() != null) {
// Some simple Jabber Message
MessageEvent event = EventFactory.createMessageEvent(JabberTransport.this, getFrom(message), message.getBody());
if (message.getThread() != null) {
myUser2Thread.put(getFrom(message), message.getThread());
}
getBroadcaster().fireEvent(event);
}
}
private void processAndSendResponse(Element element, Message message) {
Element response = new Element(RESPONSE, Transport.NAMESPACE);
XmlResponseProvider provider = XmlResponseProvider.getProvider(element, getBroadcaster());
if (provider.processAndFillResponse(response, element, JabberTransport.this, getFrom(message))) {
Message responseMessage = new Message(getFrom(message));
responseMessage.addExtension(new JDOMExtension(response));
responseMessage.setThread(message.getThread());
myFacade.getConnection().sendPacket(responseMessage);
}
}
private String getFrom(Message message) {
return getSimpleId(message.getFrom());
}
}
private class MyReconnectRunnable implements Runnable {
@Override
public void run() {
try {
Thread.sleep(myReconnectTimeout);
if (myFacade.connect() != null && myFacade.getMyAccount().isLoginAllowed()) {
myReconnectProcess = myIdeFacade.runOnPooledThread(this);
}
} catch (InterruptedException ignored) {
// return
}
}
}
}