/*
* SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
*
* Distributable under LGPL license. See terms of license at gnu.org.
*/
package net.java.sip.communicator.impl.protocol.jabber;
import java.util.*;
import java.util.regex.*;
import org.osgi.framework.*;
import net.java.sip.communicator.service.argdelegation.*;
import net.java.sip.communicator.service.gui.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
/**
* The jabber implementation of the URI handler. This class handles xmpp URIs by
* trying to establish a chat with them or add you to a chatroom.
*
* @author Emil Ivov
* @author Damian Minkov
*/
public class UriHandlerJabberImpl
implements UriHandler, ServiceListener, AccountManagerListener
{
private static final Logger logger =
Logger.getLogger(UriHandlerJabberImpl.class);
/**
* The protocol provider factory that created us.
*/
private final ProtocolProviderFactory protoFactory;
/**
* A reference to the OSGi registration we create with this handler.
*/
private ServiceRegistration ourServiceRegistration = null;
/**
* The object that we are using to synchronize our service registration.
*/
private final Object registrationLock = new Object();
/**
* The AccountManager
which loads the stored accounts of
* {@link #protoFactory} and to be monitored when the mentioned loading is
* complete so that any pending {@link #uris} can be handled
*/
private AccountManager accountManager;
/**
* The indicator (and its synchronization lock) which determines whether the
* stored accounts of {@link #protoFactory} have already been loaded.
*
* Before the loading of the stored accounts (even if there're none) of the
* protoFactory
is complete, no handling of URIs is to be
* performed because there's neither information which account to handle the
* URI in case there're stored accounts available nor ground for warning the
* user a registered account is necessary to handle URIs at all in case
* there're no stored accounts.
*
*/
private final boolean[] storedAccountsAreLoaded = new boolean[1];
/**
* The list of URIs which have received requests for handling before the
* stored accounts of the {@link #protoFactory} have been loaded. They will
* be handled as soon as the mentioned loading completes.
*/
private List uris;
/**
* Marks network fails in order to avoid endless loops.
*/
private boolean networkFailReceived = false;
/**
* Creates an instance of this uri handler, so that it would start handling
* URIs by passing them to the providers registered by protoFactory
* .
*
* @param protoFactory the provider that created us.
*
* @throws NullPointerException if protoFactory is null.
*/
public UriHandlerJabberImpl(ProtocolProviderFactory protoFactory)
throws NullPointerException
{
if (protoFactory == null)
{
throw new NullPointerException(
"The ProtocolProviderFactory that a UriHandler is created with "
+ " cannot be null.");
}
this.protoFactory = protoFactory;
hookStoredAccounts();
this.protoFactory.getBundleContext().addServiceListener(this);
/*
* Registering the UriHandler isn't strictly necessary if the
* requirement to register the protoFactory after creating this instance
* is met.
*/
registerHandlerService();
}
/**
* Disposes of this UriHandler
by, for example, removing the
* listeners it has added in its constructor (in order to prevent memory
* leaks, for one).
*/
public void dispose()
{
protoFactory.getBundleContext().removeServiceListener(this);
unregisterHandlerService();
unhookStoredAccounts();
}
/**
* Sets up (if not set up already) listening for the loading of the stored
* accounts of {@link #protoFactory} in order to make it possible to
* discover when the prerequisites for handling URIs are met.
*/
private void hookStoredAccounts()
{
if (accountManager == null)
{
BundleContext bundleContext = protoFactory.getBundleContext();
accountManager =
(AccountManager) bundleContext.getService(bundleContext
.getServiceReference(AccountManager.class.getName()));
accountManager.addListener(this);
}
}
/**
* Reverts (if not reverted already) the setup performed by a previous chat
* to {@link #hookStoredAccounts()}.
*/
private void unhookStoredAccounts()
{
if (accountManager != null)
{
accountManager.removeListener(this);
accountManager = null;
}
}
/*
* (non-Javadoc)
*
* @see
* net.java.sip.communicator.service.protocol.event.AccountManagerListener
* #handleAccountManagerEvent
* (net.java.sip.communicator.service.protocol.event.AccountManagerEvent)
*/
public void handleAccountManagerEvent(AccountManagerEvent event)
{
/*
* When the loading of the stored accounts of protoFactory is complete,
* the prerequisites for handling URIs have been met so it's time to
* load any handling requests which have come before the loading and
* were thus delayed in uris.
*/
if ((AccountManagerEvent.STORED_ACCOUNTS_LOADED == event.getType())
&& (protoFactory == event.getFactory()))
{
List uris = null;
synchronized (storedAccountsAreLoaded)
{
storedAccountsAreLoaded[0] = true;
if (this.uris != null)
{
uris = this.uris;
this.uris = null;
}
}
unhookStoredAccounts();
if (uris != null)
{
for (Iterator uriIter = uris.iterator(); uriIter
.hasNext();)
{
handleUri(uriIter.next());
}
}
}
}
/**
* Registers this UriHandler with the bundle context so that it could start
* handling URIs
*/
public void registerHandlerService()
{
synchronized (registrationLock)
{
if (ourServiceRegistration != null)
{
// ... we are already registered (this is probably
// happening during startup)
return;
}
Hashtable registrationProperties =
new Hashtable();
registrationProperties.put(UriHandler.PROTOCOL_PROPERTY,
getProtocol());
ourServiceRegistration =
JabberActivator.bundleContext.registerService(UriHandler.class
.getName(), this, registrationProperties);
}
}
/**
* Unregisters this UriHandler from the bundle context.
*/
public void unregisterHandlerService()
{
synchronized (registrationLock)
{
if (ourServiceRegistration != null)
{
ourServiceRegistration.unregister();
ourServiceRegistration = null;
}
}
}
/**
* Returns the protocol that this handler is responsible for or "xmpp" in
* other words.
*
* @return the "xmpp" string to indicate that this handler is responsible for
* handling "xmpp" uris.
*/
public String getProtocol()
{
return "xmpp";
}
/**
* Parses the specified URI and creates a chat with the currently active
* im operation set.
*
* @param uri the xmpp URI that we have to handle.
*/
public void handleUri(String uri)
{
/*
* TODO If the requirement to register the factory service after
* creating this instance is broken, we'll end up not handling the URIs.
*/
synchronized (storedAccountsAreLoaded)
{
if (!storedAccountsAreLoaded[0])
{
if (uris == null)
{
uris = new LinkedList();
}
uris.add(uri);
return;
}
}
ProtocolProviderService provider;
try
{
provider = selectHandlingProvider(uri);
}
catch (OperationFailedException exc)
{
// The operation has been canceled by the user. Bail out.
if (logger.isTraceEnabled())
logger.trace("User canceled handling of uri " + uri);
return;
}
// if provider is null then we need to tell the user to create an
// account
if (provider == null)
{
showErrorMessage(
"You need to configure at least one XMPP account \n"
+ "to be able to call " + uri, null);
return;
}
if(!uri.contains("?"))
{
OperationSetPersistentPresence presenceOpSet
= provider
.getOperationSet(OperationSetPersistentPresence.class);
String contactId = uri.replaceFirst(getProtocol() + ":", "");
//todo check url!!
//Set the email pattern string
Pattern p = Pattern.compile(".+@.+\\.[a-z]+");
if(!p.matcher(contactId).matches())
{
showErrorMessage(
"Wrong contact id : " + uri, null);
return;
}
Contact contact = presenceOpSet.findContactByID(contactId);
if(contact == null)
{
Object result =
JabberActivator.getUIService().getPopupDialog().
showConfirmPopupDialog(
"Do you want to add the contact : " + contactId + " ?",
"Add contact",
PopupDialog.YES_NO_OPTION);
if(result.equals(PopupDialog.YES_OPTION))
{
ExportedWindow ex = JabberActivator.getUIService().
getExportedWindow(ExportedWindow.ADD_CONTACT_WINDOW,
new String[]{contactId});
ex.setVisible(true);
}
return;
}
JabberActivator.getUIService().
getChat(contact).setChatVisible(true);
}
else
{
String croom = uri.replaceFirst(getProtocol() + ":", "");
int ix = croom.indexOf("?");
String param = croom.substring(ix + 1, croom.length());
croom = croom.substring(0, ix);
if(param.equalsIgnoreCase("join"))
{
OperationSetMultiUserChat mchatOpSet
= provider
.getOperationSet(OperationSetMultiUserChat.class);
try
{
ChatRoom room = mchatOpSet.findRoom(croom);
if(room != null)
{
room.join();
}
}
catch (OperationFailedException exc)
{
// if we are not online we get this error
// will wait for it and then will try to handle once again
if(exc.getErrorCode() == OperationFailedException.NETWORK_FAILURE
&& !networkFailReceived)
{
networkFailReceived = true;
OperationSetPresence presenceOpSet
= provider
.getOperationSet(OperationSetPresence.class);
presenceOpSet.addProviderPresenceStatusListener(
new ProviderStatusListener(uri, presenceOpSet));
}
else
showErrorMessage("Error joining to " + croom, exc);
}
catch (OperationNotSupportedException exc)
{
showErrorMessage("Join to " + croom + ", not supported!", exc);
}
}
else
showErrorMessage(
"Unknown param : " + param, null);
}
}
/**
* Informs the user that they need to be registered before chatting and
* asks them whether they would like us to do it for them.
*
* @param uri the uri that the user would like us to chat with after registering.
* @param provider the provider that we may have to reregister.
*/
private void promptForRegistration(String uri,
ProtocolProviderService provider)
{
int answer =
JabberActivator
.getUIService()
.getPopupDialog()
.showConfirmPopupDialog(
"You need to be online in order to chat and your "
+ "account is currently offline. Do want to connect now?",
"Account is currently offline", PopupDialog.YES_NO_OPTION);
if (answer == PopupDialog.YES_OPTION)
{
new ProtocolRegistrationThread(uri, provider).start();
}
}
/**
* The point of implementing a service listener here is so that we would
* only register our own uri handling service and thus only handle URIs
* while the factory is available as an OSGi service. We remove ourselves
* when our factory unregisters its service reference.
*
* @param event the OSGi ServiceEvent
*/
public void serviceChanged(ServiceEvent event)
{
Object sourceService =
JabberActivator.bundleContext.
getService(event.getServiceReference());
// ignore anything but our protocol factory.
if (sourceService != protoFactory)
{
return;
}
switch (event.getType())
{
case ServiceEvent.REGISTERED:
// our factory has just been registered as a service ...
registerHandlerService();
break;
case ServiceEvent.UNREGISTERING:
// our factory just died - seppuku.
unregisterHandlerService();
break;
default:
// we don't care.
break;
}
}
/**
* Uses the UIService to show an error message and log and
* exception.
*
* @param message the message that we'd like to show to the user.
* @param exc the exception that we'd like to log
*/
private void showErrorMessage(String message, Exception exc)
{
JabberActivator.getUIService().getPopupDialog().showMessagePopupDialog(
message, "Failed to create chat!", PopupDialog.ERROR_MESSAGE);
logger.error(message, exc);
}
/**
* We use this class when launching a provider registration by ourselves in
* order to track for provider registration states and retry uri handling,
* once the provider is registered.
*
*/
private class ProtocolRegistrationThread
extends Thread
implements RegistrationStateChangeListener
{
private ProtocolProviderService handlerProvider = null;
/**
* The URI that we'd need to chat.
*/
private String uri = null;
/**
* Configures this thread register our parent provider and re-attempt
* connection to the specified uri.
*
* @param uri the uri that we need to handle.
* @param handlerProvider the provider that we are going to make
* register and that we are going to use to handle the
* uri.
*/
public ProtocolRegistrationThread(String uri,
ProtocolProviderService handlerProvider)
{
super("UriHandlerProviderRegistrationThread:uri=" + uri);
this.uri = uri;
this.handlerProvider = handlerProvider;
}
/**
* Starts the registration process, ads this class as a registration
* listener and then tries to rehandle the uri this thread was initiaded
* with.
*/
@Override
public void run()
{
handlerProvider.addRegistrationStateChangeListener(this);
try
{
handlerProvider.register(JabberActivator.getUIService()
.getDefaultSecurityAuthority(handlerProvider));
}
catch (OperationFailedException exc)
{
logger.error("Failed to manually register provider.");
logger.warn(exc.getMessage(), exc);
}
}
/**
* If the parent provider passes into the registration state, the method
* re-handles the URI that this thread was initiated with. The method
* would only rehandle the uri if the event shows successful
* registration. It would ignore intermediate states such as
* REGISTERING. Disconnection and failure events would simply cause this
* listener to remove itself from the list of registration listeners.
*
* @param evt the RegistrationStateChangeEvent that this thread
* was initiated with.
*/
public void registrationStateChanged(RegistrationStateChangeEvent evt)
{
if (evt.getNewState() == RegistrationState.REGISTERED)
{
Thread uriRehandleThread = new Thread()
{
public void run()
{
handleUri(uri);
}
};
uriRehandleThread.setName("UriRehandleThread:uri=" + uri);
uriRehandleThread.start();
}
// we're only interested in a single event so we stop listening
// (unless this was a REGISTERING notification)
if (evt.getNewState() == RegistrationState.REGISTERING)
return;
handlerProvider.removeRegistrationStateChangeListener(this);
}
}
/**
* Returns the default provider that we are supposed to handle URIs through
* or null if there aren't any. Depending on the implementation this method
* may require user intervention so make sure you don't rely on a quick
* outcome when chatting it.
*
* @param uri the uri that we'd like to handle with the provider that we are
* about to select.
*
* @return the provider that we should handle URIs through.
*
* @throws OperationFailedException with code OPERATION_CANCELED if
* the users.
*/
public ProtocolProviderService selectHandlingProvider(String uri)
throws OperationFailedException
{
ArrayList registeredAccounts =
protoFactory.getRegisteredAccounts();
// if we don't have any providers - return null.
if (registeredAccounts.size() == 0)
{
return null;
}
// if we only have one provider - select it
if (registeredAccounts.size() == 1)
{
ServiceReference providerReference =
protoFactory.getProviderForAccount(registeredAccounts.get(0));
ProtocolProviderService provider =
(ProtocolProviderService) JabberActivator.bundleContext
.getService(providerReference);
return provider;
}
// otherwise - ask the user.
ArrayList providers =
new ArrayList();
for (AccountID accountID : registeredAccounts)
{
ServiceReference providerReference =
protoFactory.getProviderForAccount(accountID);
ProtocolProviderService provider =
(ProtocolProviderService) JabberActivator.bundleContext
.getService(providerReference);
providers.add(new ProviderComboBoxEntry(provider));
}
Object result =
JabberActivator.getUIService().getPopupDialog().showInputPopupDialog(
"Please select the account that you would like \n"
+ "to use to chat with " + uri, "Account Selection",
PopupDialog.OK_CANCEL_OPTION, providers.toArray(),
providers.get(0));
if (result == null)
{
throw new OperationFailedException("Operation cancelled",
OperationFailedException.OPERATION_CANCELED);
}
return ((ProviderComboBoxEntry) result).provider;
}
/**
* A class that we use to wrap providers before showing them to the user
* through a selection popup dialog from the UIService.
*/
private static class ProviderComboBoxEntry
{
public final ProtocolProviderService provider;
public ProviderComboBoxEntry(ProtocolProviderService provider)
{
this.provider = provider;
}
/**
* Returns a human readable String representing the provider
* encapsulated by this class.
*
* @return a human readable string representing the provider.
*/
@Override
public String toString()
{
return provider.getAccountID().getAccountAddress();
}
}
/**
* Waiting for the provider to bcome online and then handle the uri.
*/
private class ProviderStatusListener
implements ProviderPresenceStatusListener
{
private String uri;
private OperationSetPresence parentOpSet;
public ProviderStatusListener(String uri, OperationSetPresence parentOpSet)
{
this.uri = uri;
this.parentOpSet = parentOpSet;
}
public void providerStatusChanged(ProviderPresenceStatusChangeEvent evt)
{
if(evt.getNewStatus().isOnline())
{
parentOpSet.removeProviderPresenceStatusListener(this);
handleUri(uri);
}
}
public void providerStatusMessageChanged(java.beans.PropertyChangeEvent evt)
{
}
}
}