diff options
author | slewis | 2006-04-14 04:45:38 +0000 |
---|---|---|
committer | slewis | 2006-04-14 04:45:38 +0000 |
commit | 50a245a0891b6870dcd83b667c8364d5741238d6 (patch) | |
tree | 91b890df2f0122297450b519f56e7e16aabd7993 /protocols | |
parent | 917f444b93cb02a5e333041e1336af83ccdbc110 (diff) | |
download | org.eclipse.ecf-50a245a0891b6870dcd83b667c8364d5741238d6.tar.gz org.eclipse.ecf-50a245a0891b6870dcd83b667c8364d5741238d6.tar.xz org.eclipse.ecf-50a245a0891b6870dcd83b667c8364d5741238d6.zip |
Added plugin org.jivesoftware.smack plugin (v 2.2.0). Changed dependencies in org.eclipse.ecf.provider.xmpp to this new plugin rather than to smack libraries in org.eclipse.ecf.provider.xmpp/lib. Added smack plugin to build. Changed xmpp feature to include smack plugin.
Diffstat (limited to 'protocols')
187 files changed, 36821 insertions, 0 deletions
diff --git a/protocols/bundles/org.jivesoftware.smack/.classpath b/protocols/bundles/org.jivesoftware.smack/.classpath new file mode 100644 index 000000000..ae0ea80f3 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/.classpath @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry exported="true" kind="lib" path="jars/xmlpull.v1.jar"/> + <classpathentry exported="true" kind="lib" path="jars/smack.jar"/> + <classpathentry exported="true" kind="lib" path="jars/smackx.jar"/> + <classpathentry exported="true" kind="lib" path="jars/smackx-debug.jar"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/executionEnvironments/J2SE-1.4"/> + <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/protocols/bundles/org.jivesoftware.smack/.project b/protocols/bundles/org.jivesoftware.smack/.project new file mode 100644 index 000000000..247d8cc4e --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/.project @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>org.jivesoftware.smack</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.ManifestBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.SchemaBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.pde.PluginNature</nature> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/protocols/bundles/org.jivesoftware.smack/.settings/org.eclipse.jdt.core.prefs b/protocols/bundles/org.jivesoftware.smack/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..9a51ee3e8 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +#Tue Apr 11 23:32:11 PDT 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.2 +org.eclipse.jdt.core.compiler.compliance=1.4 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=warning +org.eclipse.jdt.core.compiler.problem.enumIdentifier=warning +org.eclipse.jdt.core.compiler.source=1.3 diff --git a/protocols/bundles/org.jivesoftware.smack/META-INF/MANIFEST.MF b/protocols/bundles/org.jivesoftware.smack/META-INF/MANIFEST.MF new file mode 100644 index 000000000..410f7fe20 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/META-INF/MANIFEST.MF @@ -0,0 +1,28 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: ECF Smack Plug-in +Bundle-SymbolicName: org.jivesoftware.smack;singleton:=true +Bundle-Version: 2.2.0 +Bundle-Vendor: eclipsercp.org +Bundle-Localization: plugin +Export-Package: org.jivesoftware.smack, + org.jivesoftware.smack.debugger, + org.jivesoftware.smack.filter, + org.jivesoftware.smack.packet, + org.jivesoftware.smack.provider, + org.jivesoftware.smack.sasl, + org.jivesoftware.smack.util, + org.jivesoftware.smackx, + org.jivesoftware.smackx.debugger, + org.jivesoftware.smackx.filetransfer, + org.jivesoftware.smackx.muc, + org.jivesoftware.smackx.packet, + org.jivesoftware.smackx.provider, + org.jivesoftware.smackx.search, + org.xmlpull.mxp1, + org.xmlpull.v1 +Require-Bundle: org.eclipse.core.runtime +Bundle-ClassPath: jars/xmlpull.v1.jar, + jars/smack.jar, + jars/smackx.jar, + jars/smackx-debug.jar diff --git a/protocols/bundles/org.jivesoftware.smack/build.properties b/protocols/bundles/org.jivesoftware.smack/build.properties new file mode 100644 index 000000000..dcf50694d --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/build.properties @@ -0,0 +1,12 @@ +bin.includes = META-INF/,\ + plugin.xml,\ + schema/,\ + jars/ +jars.compile.order = +src.includes = schema/,\ + plugin.xml,\ + META-INF/,\ + jars/ +jre.compilation.profile = J2SE-1.4 +bin.excludes = jars/jsse.jar +src.excludes = jars/jsse.jar diff --git a/protocols/bundles/org.jivesoftware.smack/jars/smack.jar b/protocols/bundles/org.jivesoftware.smack/jars/smack.jar Binary files differnew file mode 100644 index 000000000..dfe9d8426 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/jars/smack.jar diff --git a/protocols/bundles/org.jivesoftware.smack/jars/smackx-debug.jar b/protocols/bundles/org.jivesoftware.smack/jars/smackx-debug.jar Binary files differnew file mode 100644 index 000000000..ff9c58781 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/jars/smackx-debug.jar diff --git a/protocols/bundles/org.jivesoftware.smack/jars/smackx.jar b/protocols/bundles/org.jivesoftware.smack/jars/smackx.jar Binary files differnew file mode 100644 index 000000000..00eeeaeb3 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/jars/smackx.jar diff --git a/protocols/bundles/org.jivesoftware.smack/jars/xmlpull.v1.jar b/protocols/bundles/org.jivesoftware.smack/jars/xmlpull.v1.jar Binary files differnew file mode 100644 index 000000000..937c885a3 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/jars/xmlpull.v1.jar diff --git a/protocols/bundles/org.jivesoftware.smack/plugin.xml b/protocols/bundles/org.jivesoftware.smack/plugin.xml new file mode 100644 index 000000000..376d401db --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/plugin.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?eclipse version="3.0"?> +<plugin> + <extension-point id="providerManagers" name="Smack Extension/IQ Provider Managers" schema="schema/providerManagers.exsd"/> + <extension-point id="debuggers" name="Smack Debugger" schema="schema/debuggers.exsd"/> +</plugin> diff --git a/protocols/bundles/org.jivesoftware.smack/schema/debuggers.exsd b/protocols/bundles/org.jivesoftware.smack/schema/debuggers.exsd new file mode 100644 index 000000000..b6ec4cd4d --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/schema/debuggers.exsd @@ -0,0 +1,99 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!-- Schema file written by PDE --> +<schema targetNamespace="org.jivesoftware.smack"> +<annotation> + <appInfo> + <meta.schema plugin="org.jivesoftware.smack" id="debuggers" name="Smack Debuggers"/> + </appInfo> + <documentation> + [Enter description of this extension point.] + </documentation> + </annotation> + + <element name="extension"> + <complexType> + <sequence> + <element ref="debugger"/> + </sequence> + <attribute name="point" type="string" use="required"> + <annotation> + <documentation> + + </documentation> + </annotation> + </attribute> + <attribute name="id" type="string"> + <annotation> + <documentation> + + </documentation> + </annotation> + </attribute> + <attribute name="name" type="string"> + <annotation> + <documentation> + + </documentation> + </annotation> + </attribute> + </complexType> + </element> + + <element name="debugger"> + <complexType> + <attribute name="class" type="string" use="required"> + <annotation> + <documentation> + + </documentation> + </annotation> + </attribute> + </complexType> + </element> + + <annotation> + <appInfo> + <meta.section type="since"/> + </appInfo> + <documentation> + [Enter the first release in which this extension point appears.] + </documentation> + </annotation> + + <annotation> + <appInfo> + <meta.section type="examples"/> + </appInfo> + <documentation> + [Enter extension point usage example here.] + </documentation> + </annotation> + + <annotation> + <appInfo> + <meta.section type="apiInfo"/> + </appInfo> + <documentation> + [Enter API information here.] + </documentation> + </annotation> + + <annotation> + <appInfo> + <meta.section type="implementation"/> + </appInfo> + <documentation> + [Enter information about supplied implementation of this extension point.] + </documentation> + </annotation> + + <annotation> + <appInfo> + <meta.section type="copyright"/> + </appInfo> + <documentation> + + </documentation> + </annotation> + +</schema> diff --git a/protocols/bundles/org.jivesoftware.smack/schema/providerManagers.exsd b/protocols/bundles/org.jivesoftware.smack/schema/providerManagers.exsd new file mode 100644 index 000000000..2822b5238 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/schema/providerManagers.exsd @@ -0,0 +1,99 @@ +<?xml version='1.0' encoding='UTF-8'?> +<!-- Schema file written by PDE --> +<schema targetNamespace="org.jivesoftware.smack"> +<annotation> + <appInfo> + <meta.schema plugin="org.jivesoftware.smack" id="providerManagers" name="Smack Extension/IQ Provider Managers"/> + </appInfo> + <documentation> + [Enter description of this extension point.] + </documentation> + </annotation> + + <element name="extension"> + <complexType> + <sequence> + <element ref="manager"/> + </sequence> + <attribute name="point" type="string" use="required"> + <annotation> + <documentation> + + </documentation> + </annotation> + </attribute> + <attribute name="id" type="string"> + <annotation> + <documentation> + + </documentation> + </annotation> + </attribute> + <attribute name="name" type="string"> + <annotation> + <documentation> + + </documentation> + </annotation> + </attribute> + </complexType> + </element> + + <element name="manager"> + <complexType> + <attribute name="class" type="string" use="required"> + <annotation> + <documentation> + + </documentation> + </annotation> + </attribute> + </complexType> + </element> + + <annotation> + <appInfo> + <meta.section type="since"/> + </appInfo> + <documentation> + [Enter the first release in which this extension point appears.] + </documentation> + </annotation> + + <annotation> + <appInfo> + <meta.section type="examples"/> + </appInfo> + <documentation> + [Enter extension point usage example here.] + </documentation> + </annotation> + + <annotation> + <appInfo> + <meta.section type="apiInfo"/> + </appInfo> + <documentation> + [Enter API information here.] + </documentation> + </annotation> + + <annotation> + <appInfo> + <meta.section type="implementation"/> + </appInfo> + <documentation> + [Enter information about supplied implementation of this extension point.] + </documentation> + </annotation> + + <annotation> + <appInfo> + <meta.section type="copyright"/> + </appInfo> + <documentation> + + </documentation> + </annotation> + +</schema> diff --git a/protocols/bundles/org.jivesoftware.smack/src/META-INF/smack-config.xml b/protocols/bundles/org.jivesoftware.smack/src/META-INF/smack-config.xml new file mode 100644 index 000000000..eb0ea25ce --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/META-INF/smack-config.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- Smack configuration file. --> +<smack> + + <!-- Classes that will be loaded when Smack starts --> + <startupClasses> + <className>org.jivesoftware.smackx.ServiceDiscoveryManager</className> + <className>org.jivesoftware.smackx.XHTMLManager</className> + <className>org.jivesoftware.smackx.muc.MultiUserChat</className> + <className>org.jivesoftware.smackx.filetransfer.FileTransferManager</className> + </startupClasses> + + <!-- Paket reply timeout in milliseconds --> + <packetReplyTimeout>5000</packetReplyTimeout> + + <!-- Keep-alive interval in milleseconds --> + <keepAliveInterval>30000</keepAliveInterval> + +</smack>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/META-INF/smack.providers b/protocols/bundles/org.jivesoftware.smack/src/META-INF/smack.providers new file mode 100644 index 000000000..98867a4fa --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/META-INF/smack.providers @@ -0,0 +1,183 @@ +<?xml version="1.0"?> +<!-- Providers file for default Smack extensions --> +<smackProviders> + + <!-- Private Data Storage --> + <iqProvider> + <elementName>query</elementName> + <namespace>jabber:iq:private</namespace> + <className>org.jivesoftware.smackx.PrivateDataManager$PrivateDataIQProvider</className> + </iqProvider> + + <!-- Time --> + <iqProvider> + <elementName>query</elementName> + <namespace>jabber:iq:time</namespace> + <className>org.jivesoftware.smackx.packet.Time</className> + </iqProvider> + + <!-- Roster Exchange --> + <extensionProvider> + <elementName>x</elementName> + <namespace>jabber:x:roster</namespace> + <className>org.jivesoftware.smackx.provider.RosterExchangeProvider</className> + </extensionProvider> + + <!-- Message Events --> + <extensionProvider> + <elementName>x</elementName> + <namespace>jabber:x:event</namespace> + <className>org.jivesoftware.smackx.provider.MessageEventProvider</className> + </extensionProvider> + + <!-- XHTML --> + <extensionProvider> + <elementName>html</elementName> + <namespace>http://jabber.org/protocol/xhtml-im</namespace> + <className>org.jivesoftware.smackx.provider.XHTMLExtensionProvider</className> + </extensionProvider> + + <!-- Group Chat Invitations --> + <extensionProvider> + <elementName>x</elementName> + <namespace>jabber:x:conference</namespace> + <className>org.jivesoftware.smackx.GroupChatInvitation$Provider</className> + </extensionProvider> + + <!-- Service Discovery # Items --> + <iqProvider> + <elementName>query</elementName> + <namespace>http://jabber.org/protocol/disco#items</namespace> + <className>org.jivesoftware.smackx.provider.DiscoverItemsProvider</className> + </iqProvider> + + <!-- Service Discovery # Info --> + <iqProvider> + <elementName>query</elementName> + <namespace>http://jabber.org/protocol/disco#info</namespace> + <className>org.jivesoftware.smackx.provider.DiscoverInfoProvider</className> + </iqProvider> + + <!-- Data Forms--> + <extensionProvider> + <elementName>x</elementName> + <namespace>jabber:x:data</namespace> + <className>org.jivesoftware.smackx.provider.DataFormProvider</className> + </extensionProvider> + + <!-- MUC User --> + <extensionProvider> + <elementName>x</elementName> + <namespace>http://jabber.org/protocol/muc#user</namespace> + <className>org.jivesoftware.smackx.provider.MUCUserProvider</className> + </extensionProvider> + + <!-- MUC Admin --> + <iqProvider> + <elementName>query</elementName> + <namespace>http://jabber.org/protocol/muc#admin</namespace> + <className>org.jivesoftware.smackx.provider.MUCAdminProvider</className> + </iqProvider> + + <!-- MUC Owner --> + <iqProvider> + <elementName>query</elementName> + <namespace>http://jabber.org/protocol/muc#owner</namespace> + <className>org.jivesoftware.smackx.provider.MUCOwnerProvider</className> + </iqProvider> + + <!-- Delayed Delivery --> + <extensionProvider> + <elementName>x</elementName> + <namespace>jabber:x:delay</namespace> + <className>org.jivesoftware.smackx.provider.DelayInformationProvider</className> + </extensionProvider> + + <!-- Version --> + <iqProvider> + <elementName>query</elementName> + <namespace>jabber:iq:version</namespace> + <className>org.jivesoftware.smackx.packet.Version</className> + </iqProvider> + + <!-- VCard --> + <iqProvider> + <elementName>vCard</elementName> + <namespace>vcard-temp</namespace> + <className>org.jivesoftware.smackx.provider.VCardProvider</className> + </iqProvider> + + <!-- Offline Message Requests --> + <iqProvider> + <elementName>offline</elementName> + <namespace>http://jabber.org/protocol/offline</namespace> + <className>org.jivesoftware.smackx.packet.OfflineMessageRequest$Provider</className> + </iqProvider> + + <!-- Offline Message Indicator --> + <extensionProvider> + <elementName>offline</elementName> + <namespace>http://jabber.org/protocol/offline</namespace> + <className>org.jivesoftware.smackx.packet.OfflineMessageInfo$Provider</className> + </extensionProvider> + + <!-- Last Activity --> + <iqProvider> + <elementName>query</elementName> + <namespace>jabber:iq:last</namespace> + <className>org.jivesoftware.smackx.packet.LastActivity$Provider</className> + </iqProvider> + + <!-- User Search --> + <iqProvider> + <elementName>query</elementName> + <namespace>jabber:iq:search</namespace> + <className>org.jivesoftware.smackx.search.UserSearch$Provider</className> + </iqProvider> + + <!-- SharedGroupsInfo --> + <iqProvider> + <elementName>sharedgroup</elementName> + <namespace>http://www.jivesoftware.org/protocol/sharedgroup</namespace> + <className>org.jivesoftware.smackx.packet.SharedGroupsInfo$Provider</className> + </iqProvider> + + <!-- JEP-33: Extended Stanza Addressing --> + <extensionProvider> + <elementName>addresses</elementName> + <namespace>http://jabber.org/protocol/address</namespace> + <className>org.jivesoftware.smackx.provider.MultipleAddressesProvider</className> + </extensionProvider> + + <!-- FileTransfer --> + <iqProvider> + <elementName>si</elementName> + <namespace>http://jabber.org/protocol/si</namespace> + <className>org.jivesoftware.smackx.provider.StreamInitiationProvider</className> + </iqProvider> + + <iqProvider> + <elementName>query</elementName> + <namespace>http://jabber.org/protocol/bytestreams</namespace> + <className>org.jivesoftware.smackx.provider.BytestreamsProvider</className> + </iqProvider> + + <iqProvider> + <elementName>open</elementName> + <namespace>http://jabber.org/protocol/ibb</namespace> + <className>org.jivesoftware.smackx.provider.IBBProviders$Open</className> + </iqProvider> + + <iqProvider> + <elementName>close</elementName> + <namespace>http://jabber.org/protocol/ibb</namespace> + <className>org.jivesoftware.smackx.provider.IBBProviders$Close</className> + </iqProvider> + + <extensionProvider> + <elementName>data</elementName> + <namespace>http://jabber.org/protocol/ibb</namespace> + <className>org.jivesoftware.smackx.provider.IBBProviders$Data</className> + </extensionProvider> + +</smackProviders>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/AccountManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/AccountManager.java new file mode 100644 index 000000000..7fb515d7b --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/AccountManager.java @@ -0,0 +1,298 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.packet.Registration; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.filter.*; +import org.jivesoftware.smack.util.StringUtils; + +import java.util.*; + +/** + * Allows creation and management of accounts on an XMPP server. + * + * @see XMPPConnection#getAccountManager() + * @author Matt Tucker + */ +public class AccountManager { + + private XMPPConnection connection; + private Registration info = null; + + /** + * Creates a new AccountManager instance. + * + * @param connection a connection to a XMPP server. + */ + public AccountManager(XMPPConnection connection) { + this.connection = connection; + } + + /** + * Returns true if the server supports creating new accounts. Many servers require + * that you not be currently authenticated when creating new accounts, so the safest + * behavior is to only create new accounts before having logged in to a server. + * + * @return true if the server support creating new accounts. + */ + public boolean supportsAccountCreation() { + try { + if (info == null) { + getRegistrationInfo(); + } + return info.getType() != IQ.Type.ERROR; + } + catch (XMPPException xe) { + return false; + } + } + + /** + * Returns an Iterator for the (String) names of the required account attributes. + * All attributes must be set when creating new accounts. The standard + * attributes are as follows: <ul> + * <li>name -- the user's name. + * <li>first -- the user's first name. + * <li>last -- the user's last name. + * <li>email -- the user's email address. + * <li>city -- the user's city. + * <li>state -- the user's state. + * <li>zip -- the user's ZIP code. + * <li>phone -- the user's phone number. + * <li>url -- the user's website. + * <li>date -- the date the registration took place. + * <li>misc -- other miscellaneous information to associate with the account. + * <li>text -- textual information to associate with the account. + * <li>remove -- empty flag to remove account. + * </ul><p> + * + * Typically, servers require no attributes when creating new accounts, or just + * the user's email address. + * + * @return the required account attributes. + */ + public Iterator getAccountAttributes() { + try { + if (info == null) { + getRegistrationInfo(); + } + Map attributes = info.getAttributes(); + if (attributes != null) { + return attributes.keySet().iterator(); + } + } + catch (XMPPException xe) { } + return Collections.EMPTY_LIST.iterator(); + } + + /** + * Returns the value of a given account attribute or <tt>null</tt> if the account + * attribute wasn't found. + * + * @param name the name of the account attribute to return its value. + * @return the value of the account attribute or <tt>null</tt> if an account + * attribute wasn't found for the requested name. + */ + public String getAccountAttribute(String name) { + try { + if (info == null) { + getRegistrationInfo(); + } + return (String) info.getAttributes().get(name); + } + catch (XMPPException xe) { } + return null; + } + + /** + * Returns the instructions for creating a new account, or <tt>null</tt> if there + * are no instructions. If present, instructions should be displayed to the end-user + * that will complete the registration process. + * + * @return the account creation instructions, or <tt>null</tt> if there are none. + */ + public String getAccountInstructions() { + try { + if (info == null) { + getRegistrationInfo(); + } + return info.getInstructions(); + } + catch (XMPPException xe) { + return null; + } + } + + /** + * Creates a new account using the specified username and password. The server may + * require a number of extra account attributes such as an email address and phone + * number. In that case, Smack will attempt to automatically set all required + * attributes with blank values, which may or may not be accepted by the server. + * Therefore, it's recommended to check the required account attributes and to let + * the end-user populate them with real values instead. + * + * @param username the username. + * @param password the password. + * @throws XMPPException if an error occurs creating the account. + */ + public void createAccount(String username, String password) throws XMPPException { + if (!supportsAccountCreation()) { + throw new XMPPException("Server does not support account creation."); + } + // Create a map for all the required attributes, but give them blank values. + Map attributes = new HashMap(); + for (Iterator i=getAccountAttributes(); i.hasNext(); ) { + String attributeName = (String)i.next(); + attributes.put(attributeName, ""); + } + createAccount(username, password, attributes); + } + + /** + * Creates a new account using the specified username, password and account attributes. + * The attributes Map must contain only String name/value pairs and must also have values + * for all required attributes. + * + * @param username the username. + * @param password the password. + * @param attributes the account attributes. + * @throws XMPPException if an error occurs creating the account. + * @see #getAccountAttributes() + */ + public void createAccount(String username, String password, Map attributes) + throws XMPPException + { + if (!supportsAccountCreation()) { + throw new XMPPException("Server does not support account creation."); + } + Registration reg = new Registration(); + reg.setType(IQ.Type.SET); + reg.setTo(connection.getServiceName()); + attributes.put("username",username); + attributes.put("password",password); + reg.setAttributes(attributes); + PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()), + new PacketTypeFilter(IQ.class)); + PacketCollector collector = connection.createPacketCollector(filter); + connection.sendPacket(reg); + IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from server."); + } + else if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + } + + /** + * Changes the password of the currently logged-in account. This operation can only + * be performed after a successful login operation has been completed. Not all servers + * support changing passwords; an XMPPException will be thrown when that is the case. + * + * @throws IllegalStateException if not currently logged-in to the server. + * @throws XMPPException if an error occurs when changing the password. + */ + public void changePassword(String newPassword) throws XMPPException { + Registration reg = new Registration(); + reg.setType(IQ.Type.SET); + reg.setTo(connection.getServiceName()); + HashMap map = new HashMap(); + map.put("username",StringUtils.parseName(connection.getUser())); + map.put("password",newPassword); + reg.setAttributes(map); + PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()), + new PacketTypeFilter(IQ.class)); + PacketCollector collector = connection.createPacketCollector(filter); + connection.sendPacket(reg); + IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from server."); + } + else if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + } + + /** + * Deletes the currently logged-in account from the server. This operation can only + * be performed after a successful login operation has been completed. Not all servers + * support deleting accounts; an XMPPException will be thrown when that is the case. + * + * @throws IllegalStateException if not currently logged-in to the server. + * @throws XMPPException if an error occurs when deleting the account. + */ + public void deleteAccount() throws XMPPException { + if (!connection.isAuthenticated()) { + throw new IllegalStateException("Must be logged in to delete a account."); + } + Registration reg = new Registration(); + reg.setType(IQ.Type.SET); + reg.setTo(connection.getServiceName()); + Map attributes = new HashMap(); + // To delete an account, we add a single attribute, "remove", that is blank. + attributes.put("remove", ""); + reg.setAttributes(attributes); + PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()), + new PacketTypeFilter(IQ.class)); + PacketCollector collector = connection.createPacketCollector(filter); + connection.sendPacket(reg); + IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from server."); + } + else if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + } + + /** + * Gets the account registration info from the server. + * + * @throws XMPPException if an error occurs. + */ + private synchronized void getRegistrationInfo() throws XMPPException { + Registration reg = new Registration(); + reg.setTo(connection.getServiceName()); + PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()), + new PacketTypeFilter(IQ.class)); + PacketCollector collector = connection.createPacketCollector(filter); + connection.sendPacket(reg); + IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from server."); + } + else if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + else { + info = (Registration)result; + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Chat.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Chat.java new file mode 100644 index 000000000..57e1633e4 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Chat.java @@ -0,0 +1,270 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.filter.*; + +import java.util.*; +import java.lang.ref.WeakReference; + +/** + * A chat is a series of messages sent between two users. Each chat has a unique + * thread ID, which is used to track which messages are part of a particular + * conversation. Some messages are sent without a thread ID, and some clients + * don't send thread IDs at all. Therefore, if a message without a thread ID + * arrives it is routed to the most recently created Chat with the message + * sender. + * + * @see XMPPConnection#createChat(String) + * @author Matt Tucker + */ +public class Chat { + + /** + * A prefix helps to make sure that ID's are unique across mutliple instances. + */ + private static String prefix = StringUtils.randomString(5); + + /** + * Keeps track of the current increment, which is appended to the prefix to + * forum a unique ID. + */ + private static long id = 0; + + /** + * Returns the next unique id. Each id made up of a short alphanumeric + * prefix along with a unique numeric value. + * + * @return the next id. + */ + private static synchronized String nextID() { + return prefix + Long.toString(id++); + } + + private XMPPConnection connection; + private String threadID; + private String participant; + private PacketFilter messageFilter; + private PacketCollector messageCollector; + private Set listeners = new HashSet(); + + /** + * Creates a new chat with the specified user. + * + * @param connection the connection the chat will use. + * @param participant the user to chat with. + */ + public Chat(XMPPConnection connection, String participant) { + // Automatically assign the next chat ID. + this(connection, participant, nextID()); + } + + /** + * Creates a new chat with the specified user and thread ID. + * + * @param connection the connection the chat will use. + * @param participant the user to chat with. + * @param threadID the thread ID to use. + */ + public Chat(XMPPConnection connection, String participant, String threadID) { + this.connection = connection; + this.participant = participant; + this.threadID = threadID; + + // Register with the map of chats so that messages with no thread ID + // set will be delivered to this Chat. + connection.chats.put(StringUtils.parseBareAddress(participant), + new WeakReference(this)); + + // Filter the messages whose thread equals Chat's id + messageFilter = new ThreadFilter(threadID); + + messageCollector = connection.createPacketCollector(messageFilter); + } + + /** + * Returns the thread id associated with this chat, which corresponds to the + * <tt>thread</tt> field of XMPP messages. This method may return <tt>null</tt> + * if there is no thread ID is associated with this Chat. + * + * @return the thread ID of this chat. + */ + public String getThreadID() { + return threadID; + } + + /** + * Returns the name of the user the chat is with. + * + * @return the name of the user the chat is occuring with. + */ + public String getParticipant() { + return participant; + } + + /** + * Sends the specified text as a message to the other chat participant. + * This is a convenience method for: + * + * <pre> + * Message message = chat.createMessage(); + * message.setBody(messageText); + * chat.sendMessage(message); + * </pre> + * + * @param text the text to send. + * @throws XMPPException if sending the message fails. + */ + public void sendMessage(String text) throws XMPPException { + Message message = createMessage(); + message.setBody(text); + connection.sendPacket(message); + } + + /** + * Creates a new Message to the chat participant. The message returned + * will have its thread property set with this chat ID. + * + * @return a new message addressed to the chat participant and + * using the correct thread value. + * @see #sendMessage(Message) + */ + public Message createMessage() { + Message message = new Message(participant, Message.Type.CHAT); + message.setThread(threadID); + return message; + } + + /** + * Sends a message to the other chat participant. The thread ID, recipient, + * and message type of the message will automatically set to those of this chat + * in case the Message was not created using the {@link #createMessage() createMessage} + * method. + * + * @param message the message to send. + * @throws XMPPException if an error occurs sending the message. + */ + public void sendMessage(Message message) throws XMPPException { + // Force the recipient, message type, and thread ID since the user elected + // to send the message through this chat object. + message.setTo(participant); + message.setType(Message.Type.CHAT); + message.setThread(threadID); + connection.sendPacket(message); + } + + /** + * Polls for and returns the next message, or <tt>null</tt> if there isn't + * a message immediately available. This method provides significantly different + * functionalty than the {@link #nextMessage()} method since it's non-blocking. + * In other words, the method call will always return immediately, whereas the + * nextMessage method will return only when a message is available (or after + * a specific timeout). + * + * @return the next message if one is immediately available and + * <tt>null</tt> otherwise. + */ + public Message pollMessage() { + return (Message)messageCollector.pollResult(); + } + + /** + * Returns the next available message in the chat. The method call will block + * (not return) until a message is available. + * + * @return the next message. + */ + public Message nextMessage() { + return (Message)messageCollector.nextResult(); + } + + /** + * Returns the next available message in the chat. The method call will block + * (not return) until a packet is available or the <tt>timeout</tt> has elapased. + * If the timeout elapses without a result, <tt>null</tt> will be returned. + * + * @param timeout the maximum amount of time to wait for the next message. + * @return the next message, or <tt>null</tt> if the timeout elapses without a + * message becoming available. + */ + public Message nextMessage(long timeout) { + return (Message)messageCollector.nextResult(timeout); + } + + /** + * Adds a packet listener that will be notified of any new messages in the + * chat. + * + * @param listener a packet listener. + */ + public void addMessageListener(PacketListener listener) { + connection.addPacketListener(listener, messageFilter); + // Keep track of the listener so that we can manually deliver extra + // messages to it later if needed. + synchronized (listeners) { + listeners.add(new WeakReference(listener)); + } + } + + /** + * Delivers a message directly to this chat, which will add the message + * to the collector and deliver it to all listeners registered with the + * Chat. This is used by the XMPPConnection class to deliver messages + * without a thread ID. + * + * @param message the message. + */ + void deliver(Message message) { + // Because the collector and listeners are expecting a thread ID with + // a specific value, set the thread ID on the message even though it + // probably never had one. + message.setThread(threadID); + + messageCollector.processPacket(message); + synchronized (listeners) { + for (Iterator i=listeners.iterator(); i.hasNext(); ) { + WeakReference listenerRef = (WeakReference)i.next(); + PacketListener listener; + if ((listener = (PacketListener)listenerRef.get()) != null) { + listener.processPacket(message); + } + // If the reference was cleared, remove it from the set. + else { + i.remove(); + } + } + } + } + + public void finalize() throws Throwable { + super.finalize(); + try { + if (messageCollector != null) { + messageCollector.cancel(); + } + } + catch (Exception e) { + // Ignore. + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionConfiguration.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionConfiguration.java new file mode 100644 index 000000000..63b6127c3 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionConfiguration.java @@ -0,0 +1,357 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import java.io.File; + +/** + * Configuration to use while establishing the connection to the server. It is possible to + * configure the path to the trustore file that keeps the trusted CA root certificates and + * enable or disable all or some of the checkings done while verifying server certificates.<p> + * + * It is also possible to configure it TLs, SASL or compression are going to be used or not. + * + * @author Gaston Dombiak + */ +public class ConnectionConfiguration implements Cloneable { + + private String serviceName; + + private String host; + private int port; + + private String truststorePath; + private String truststoreType; + private String truststorePassword; + private boolean tlsEnabled = true; + private boolean verifyChainEnabled = false; + private boolean verifyRootCAEnabled = false; + private boolean selfSignedCertificateEnabled = false; + private boolean expiredCertificatesCheckEnabled = false; + private boolean notMatchingDomainCheckEnabled = false; + + private boolean compressionEnabled = false; + + private boolean saslAuthenticationEnabled = true; + + private boolean debuggerEnabled = XMPPConnection.DEBUG_ENABLED; + + public ConnectionConfiguration(String host, int port, String serviceName) { + this.host = host; + this.port = port; + this.serviceName = serviceName; + + // Build the default path to the cacert truststore file. By default we are + // going to use the file located in $JREHOME/lib/security/cacerts. + String javaHome = System.getProperty("java.home"); + StringBuffer buffer = new StringBuffer(); + buffer.append(javaHome).append(File.separator).append("lib"); + buffer.append(File.separator).append("security"); + buffer.append(File.separator).append("cacerts"); + truststorePath = buffer.toString(); + // Set the default store type + truststoreType = "jks"; + // Set the default password of the cacert file that is "changeit" + truststorePassword = "changeit"; + } + + public ConnectionConfiguration(String host, int port) { + this(host, port, host); + } + + /** + * Returns the server name of the target server. + * + * @return the server name of the target server. + */ + public String getServiceName() { + return serviceName; + } + + /** + * Returns the host to use when establishing the connection. The host and port to use + * might have been resolved by a DNS lookup as specified by the XMPP spec. + * + * @return the host to use when establishing the connection. + */ + public String getHost() { + return host; + } + + /** + * Returns the port to use when establishing the connection. The host and port to use + * might have been resolved by a DNS lookup as specified by the XMPP spec. + * + * @return the port to use when establishing the connection. + */ + public int getPort() { + return port; + } + + /** + * Returns true if the client is going to try to secure the connection using TLS after + * the connection has been established. + * + * @return true if the client is going to try to secure the connection using TLS after + * the connection has been established. + */ + public boolean isTLSEnabled() { + return tlsEnabled; + } + + /** + * Sets if the client is going to try to secure the connection using TLS after + * the connection has been established. + * + * @param tlsEnabled if the client is going to try to secure the connection using TLS after + * the connection has been established. + */ + public void setTLSEnabled(boolean tlsEnabled) { + this.tlsEnabled = tlsEnabled; + } + + /** + * Retuns the path to the truststore file. The truststore file contains the root + * certificates of several well?known CAs. By default Smack is going to use + * the file located in $JREHOME/lib/security/cacerts. + * + * @return the path to the truststore file. + */ + public String getTruststorePath() { + return truststorePath; + } + + /** + * Sets the path to the truststore file. The truststore file contains the root + * certificates of several well?known CAs. By default Smack is going to use + * the file located in $JREHOME/lib/security/cacerts. + * + * @param truststorePath the path to the truststore file. + */ + public void setTruststorePath(String truststorePath) { + this.truststorePath = truststorePath; + } + + public String getTruststoreType() { + return truststoreType; + } + + public void setTruststoreType(String truststoreType) { + this.truststoreType = truststoreType; + } + + /** + * Returns the password to use to access the truststore file. It is assumed that all + * certificates share the same password of the truststore file. + * + * @return the password to use to access the truststore file. + */ + public String getTruststorePassword() { + return truststorePassword; + } + + /** + * Sets the password to use to access the truststore file. It is assumed that all + * certificates share the same password of the truststore file. + * + * + * @param truststorePassword the password to use to access the truststore file. + */ + public void setTruststorePassword(String truststorePassword) { + this.truststorePassword = truststorePassword; + } + + /** + * Returns true if the whole chain of certificates presented by the server are going to + * be checked. By default the certificate chain is not verified. + * + * @return true if the whole chaing of certificates presented by the server are going to + * be checked. + */ + public boolean isVerifyChainEnabled() { + return verifyChainEnabled; + } + + /** + * Sets if the whole chain of certificates presented by the server are going to + * be checked. By default the certificate chain is not verified. + * + * @param verifyChainEnabled if the whole chaing of certificates presented by the server + * are going to be checked. + */ + public void setVerifyChainEnabled(boolean verifyChainEnabled) { + this.verifyChainEnabled = verifyChainEnabled; + } + + /** + * Returns true if root CA checking is going to be done. By default checking is disabled. + * + * @return true if root CA checking is going to be done. + */ + public boolean isVerifyRootCAEnabled() { + return verifyRootCAEnabled; + } + + /** + * Sets if root CA checking is going to be done. By default checking is disabled. + * + * @param verifyRootCAEnabled if root CA checking is going to be done. + */ + public void setVerifyRootCAEnabled(boolean verifyRootCAEnabled) { + this.verifyRootCAEnabled = verifyRootCAEnabled; + } + + /** + * Returns true if self-signed certificates are going to be accepted. By default + * this option is disabled. + * + * @return true if self-signed certificates are going to be accepted. + */ + public boolean isSelfSignedCertificateEnabled() { + return selfSignedCertificateEnabled; + } + + /** + * Sets if self-signed certificates are going to be accepted. By default + * this option is disabled. + * + * @param selfSignedCertificateEnabled if self-signed certificates are going to be accepted. + */ + public void setSelfSignedCertificateEnabled(boolean selfSignedCertificateEnabled) { + this.selfSignedCertificateEnabled = selfSignedCertificateEnabled; + } + + /** + * Returns true if certificates presented by the server are going to be checked for their + * validity. By default certificates are not verified. + * + * @return true if certificates presented by the server are going to be checked for their + * validity. + */ + public boolean isExpiredCertificatesCheckEnabled() { + return expiredCertificatesCheckEnabled; + } + + /** + * Sets if certificates presented by the server are going to be checked for their + * validity. By default certificates are not verified. + * + * @param expiredCertificatesCheckEnabled if certificates presented by the server are going + * to be checked for their validity. + */ + public void setExpiredCertificatesCheckEnabled(boolean expiredCertificatesCheckEnabled) { + this.expiredCertificatesCheckEnabled = expiredCertificatesCheckEnabled; + } + + /** + * Returns true if certificates presented by the server are going to be checked for their + * domain. By default certificates are not verified. + * + * @return true if certificates presented by the server are going to be checked for their + * domain. + */ + public boolean isNotMatchingDomainCheckEnabled() { + return notMatchingDomainCheckEnabled; + } + + /** + * Sets if certificates presented by the server are going to be checked for their + * domain. By default certificates are not verified. + * + * @param notMatchingDomainCheckEnabled if certificates presented by the server are going + * to be checked for their domain. + */ + public void setNotMatchingDomainCheckEnabled(boolean notMatchingDomainCheckEnabled) { + this.notMatchingDomainCheckEnabled = notMatchingDomainCheckEnabled; + } + + /** + * Returns true if the connection is going to use stream compression. Stream compression + * will be requested after TLS was established (if TLS was enabled) and only if the server + * offered stream compression. With stream compression network traffic can be reduced + * up to 90%. By default compression is disabled. + * + * @return true if the connection is going to use stream compression. + */ + public boolean isCompressionEnabled() { + return compressionEnabled; + } + + /** + * Sets if the connection is going to use stream compression. Stream compression + * will be requested after TLS was established (if TLS was enabled) and only if the server + * offered stream compression. With stream compression network traffic can be reduced + * up to 90%. By default compression is disabled. + * + * @param compressionEnabled if the connection is going to use stream compression. + */ + public void setCompressionEnabled(boolean compressionEnabled) { + this.compressionEnabled = compressionEnabled; + } + + /** + * Returns true if the client is going to use SASL authentication when logging into the + * server. If SASL authenticatin fails then the client will try to use non-sasl authentication. + * By default SASL is enabled. + * + * @return true if the client is going to use SASL authentication when logging into the + * server. + */ + public boolean isSASLAuthenticationEnabled() { + return saslAuthenticationEnabled; + } + + /** + * Sets if the client is going to use SASL authentication when logging into the + * server. If SASL authenticatin fails then the client will try to use non-sasl authentication. + * By default SASL is enabled. + * + * @param saslAuthenticationEnabled if the client is going to use SASL authentication when + * logging into the server. + */ + public void setSASLAuthenticationEnabled(boolean saslAuthenticationEnabled) { + this.saslAuthenticationEnabled = saslAuthenticationEnabled; + } + + /** + * Returns true if the new connection about to be establish is going to be debugged. By + * default the value of {@link XMPPConnection#DEBUG_ENABLED} is used. + * + * @return true if the new connection about to be establish is going to be debugged. + */ + public boolean isDebuggerEnabled() { + return debuggerEnabled; + } + + /** + * Sets if the new connection about to be establish is going to be debugged. By + * default the value of {@link XMPPConnection#DEBUG_ENABLED} is used. + * + * @param debuggerEnabled if the new connection about to be establish is going to be debugged. + */ + public void setDebuggerEnabled(boolean debuggerEnabled) { + this.debuggerEnabled = debuggerEnabled; + } + + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionEstablishedListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionEstablishedListener.java new file mode 100644 index 000000000..316615bfc --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionEstablishedListener.java @@ -0,0 +1,41 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +/** + * Interface that allows for implementing classes to listen for connection established + * events. Listeners are registered with the XMPPConnection class. + * + * @see XMPPConnection#addConnectionListener + * @see XMPPConnection#removeConnectionListener + * + * @author Gaston Dombiak + */ +public interface ConnectionEstablishedListener { + + /** + * Notification that a new connection has been established. + * + * @param connection the new established connection + */ + public void connectionEstablished(XMPPConnection connection); + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener.java new file mode 100644 index 000000000..69bc854f3 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener.java @@ -0,0 +1,45 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +/** + * Interface that allows for implementing classes to listen for connection closing + * events. Listeners are registered with XMPPConnection objects. + * + * @see XMPPConnection#addConnectionListener + * @see XMPPConnection#removeConnectionListener + * + * @author Matt Tucker + */ +public interface ConnectionListener { + + /** + * Notification that the connection was closed normally. + */ + public void connectionClosed(); + + /** + * Notification that the connection was closed due to an exception. + * + * @param e the exception. + */ + public void connectionClosedOnError(Exception e); +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener2.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener2.java new file mode 100644 index 000000000..8a3a73174 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ConnectionListener2.java @@ -0,0 +1,10 @@ +package org.jivesoftware.smack; + +public interface ConnectionListener2 { + + /** + * Notification that the connection has been authenticated. + */ + public void connectionAuthenticated(); + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GoogleTalkConnection.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GoogleTalkConnection.java new file mode 100644 index 000000000..d81e90aab --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GoogleTalkConnection.java @@ -0,0 +1,38 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +/** + * Convenience class to make it easier to connect to the Google Talk IM service. + * You can also use {@link XMPPConnection} to connect to Google Talk by specifying + * the server name, service name, and port.<p> + * + * After creating the connection, log in in using a Gmail username and password. + * For the Gmail address "jsmith@gmail.com", the username is "jsmith". + * + * @author Matt Tucker + */ +public class GoogleTalkConnection extends XMPPConnection { + + public GoogleTalkConnection() throws XMPPException { + super("talk.google.com", 5222, "gmail.com"); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GroupChat.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GroupChat.java new file mode 100644 index 000000000..668ed8934 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/GroupChat.java @@ -0,0 +1,353 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.filter.*; + +import java.util.*; + +/** + * A GroupChat is a conversation that takes place among many users in a virtual + * room. When joining a group chat, you specify a nickname, which is the identity + * that other chat room users see. + * + * @see XMPPConnection#createGroupChat(String) + * @author Matt Tucker + */ +public class GroupChat { + + private XMPPConnection connection; + private String room; + private String nickname = null; + private boolean joined = false; + private List participants = new ArrayList(); + private List connectionListeners = new ArrayList(); + + private PacketFilter presenceFilter; + private PacketFilter messageFilter; + private PacketCollector messageCollector; + + /** + * Creates a new group chat with the specified connection and room name. Note: no + * information is sent to or received from the server until you attempt to + * {@link #join(String) join} the chat room. On some server implementations, + * the room will not be created until the first person joins it.<p> + * + * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com + * for the XMPP server example.com). You must ensure that the room address you're + * trying to connect to includes the proper chat sub-domain. + * + * @param connection the XMPP connection. + * @param room the name of the room in the form "roomName@service", where + * "service" is the hostname at which the multi-user chat + * service is running. + */ + public GroupChat(XMPPConnection connection, String room) { + this.connection = connection; + this.room = room; + // Create a collector for all incoming messages. + messageFilter = new AndFilter(new FromContainsFilter(room), + new PacketTypeFilter(Message.class)); + messageFilter = new AndFilter(messageFilter, new PacketFilter() { + public boolean accept(Packet packet) { + Message msg = (Message)packet; + return msg.getType() == Message.Type.GROUP_CHAT; + } + }); + messageCollector = connection.createPacketCollector(messageFilter); + // Create a listener for all presence updates. + presenceFilter = new AndFilter(new FromContainsFilter(room), + new PacketTypeFilter(Presence.class)); + connection.addPacketListener(new PacketListener() { + public void processPacket(Packet packet) { + Presence presence = (Presence)packet; + String from = presence.getFrom(); + if (presence.getType() == Presence.Type.AVAILABLE) { + synchronized (participants) { + if (!participants.contains(from)) { + participants.add(from); + } + } + } + else if (presence.getType() == Presence.Type.UNAVAILABLE) { + synchronized (participants) { + participants.remove(from); + } + } + } + }, presenceFilter); + } + + /** + * Returns the name of the room this GroupChat object represents. + * + * @return the groupchat room name. + */ + public String getRoom() { + return room; + } + + /** + * Joins the chat room using the specified nickname. If already joined + * using another nickname, this method will first leave the room and then + * re-join using the new nickname. The default timeout of 5 seconds for a reply + * from the group chat server that the join succeeded will be used. + * + * @param nickname the nickname to use. + * @throws XMPPException if an error occurs joining the room. In particular, a + * 409 error can occur if someone is already in the group chat with the same + * nickname. + */ + public synchronized void join(String nickname) throws XMPPException { + join(nickname, SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Joins the chat room using the specified nickname. If already joined as + * another nickname, will leave as that name first before joining under the new + * name. + * + * @param nickname the nickname to use. + * @param timeout the number of milleseconds to wait for a reply from the + * group chat that joining the room succeeded. + * @throws XMPPException if an error occurs joining the room. In particular, a + * 409 error can occur if someone is already in the group chat with the same + * nickname. + */ + public synchronized void join(String nickname, long timeout) throws XMPPException { + if (nickname == null || nickname.equals("")) { + throw new IllegalArgumentException("Nickname must not be null or blank."); + } + // If we've already joined the room, leave it before joining under a new + // nickname. + if (joined) { + leave(); + } + // We join a room by sending a presence packet where the "to" + // field is in the form "roomName@service/nickname" + Presence joinPresence = new Presence(Presence.Type.AVAILABLE); + joinPresence.setTo(room + "/" + nickname); + // Wait for a presence packet back from the server. + PacketFilter responseFilter = new AndFilter( + new FromContainsFilter(room + "/" + nickname), + new PacketTypeFilter(Presence.class)); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send join packet. + connection.sendPacket(joinPresence); + // Wait up to a certain number of seconds for a reply. + Presence presence = (Presence)response.nextResult(timeout); + response.cancel(); + if (presence == null) { + throw new XMPPException("No response from server."); + } + else if (presence.getError() != null) { + throw new XMPPException(presence.getError()); + } + this.nickname = nickname; + joined = true; + } + + /** + * Returns true if currently in the group chat (after calling the {@link + * #join(String)} method. + * + * @return true if currently in the group chat room. + */ + public boolean isJoined() { + return joined; + } + + /** + * Leave the chat room. + */ + public synchronized void leave() { + // If not joined already, do nothing. + if (!joined) { + return; + } + // We leave a room by sending a presence packet where the "to" + // field is in the form "roomName@service/nickname" + Presence leavePresence = new Presence(Presence.Type.UNAVAILABLE); + leavePresence.setTo(room + "/" + nickname); + connection.sendPacket(leavePresence); + // Reset participant information. + participants = new ArrayList(); + nickname = null; + joined = false; + } + + /** + * Returns the nickname that was used to join the room, or <tt>null</tt> if not + * currently joined. + * + * @return the nickname currently being used. + */ + public String getNickname() { + return nickname; + } + + /** + * Returns the number of participants in the group chat.<p> + * + * Note: this value will only be accurate after joining the group chat, and + * may fluctuate over time. If you query this value directly after joining the + * group chat it may not be accurate, as it takes a certain amount of time for + * the server to send all presence packets to this client. + * + * @return the number of participants in the group chat. + */ + public int getParticipantCount() { + synchronized (participants) { + return participants.size(); + } + } + + /** + * Returns an Iterator (of Strings) for the list of fully qualified participants + * in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser". + * Typically, a client would only display the nickname of the participant. To + * get the nickname from the fully qualified name, use the + * {@link org.jivesoftware.smack.util.StringUtils#parseResource(String)} method. + * Note: this value will only be accurate after joining the group chat, and may + * fluctuate over time. + * + * @return an Iterator for the participants in the group chat. + */ + public Iterator getParticipants() { + synchronized (participants) { + return Collections.unmodifiableList(new ArrayList(participants)).iterator(); + } + } + + /** + * Adds a packet listener that will be notified of any new Presence packets + * sent to the group chat. Using a listener is a suitable way to know when the list + * of participants should be re-loaded due to any changes. + * + * @param listener a packet listener that will be notified of any presence packets + * sent to the group chat. + */ + public void addParticipantListener(PacketListener listener) { + connection.addPacketListener(listener, presenceFilter); + connectionListeners.add(listener); + } + + /** + * Sends a message to the chat room. + * + * @param text the text of the message to send. + * @throws XMPPException if sending the message fails. + */ + public void sendMessage(String text) throws XMPPException { + Message message = new Message(room, Message.Type.GROUP_CHAT); + message.setBody(text); + connection.sendPacket(message); + } + + /** + * Creates a new Message to send to the chat room. + * + * @return a new Message addressed to the chat room. + */ + public Message createMessage() { + return new Message(room, Message.Type.GROUP_CHAT); + } + + /** + * Sends a Message to the chat room. + * + * @param message the message. + * @throws XMPPException if sending the message fails. + */ + public void sendMessage(Message message) throws XMPPException { + connection.sendPacket(message); + } + + /** + * Polls for and returns the next message, or <tt>null</tt> if there isn't + * a message immediately available. This method provides significantly different + * functionalty than the {@link #nextMessage()} method since it's non-blocking. + * In other words, the method call will always return immediately, whereas the + * nextMessage method will return only when a message is available (or after + * a specific timeout). + * + * @return the next message if one is immediately available and + * <tt>null</tt> otherwise. + */ + public Message pollMessage() { + return (Message)messageCollector.pollResult(); + } + + /** + * Returns the next available message in the chat. The method call will block + * (not return) until a message is available. + * + * @return the next message. + */ + public Message nextMessage() { + return (Message)messageCollector.nextResult(); + } + + /** + * Returns the next available message in the chat. The method call will block + * (not return) until a packet is available or the <tt>timeout</tt> has elapased. + * If the timeout elapses without a result, <tt>null</tt> will be returned. + * + * @param timeout the maximum amount of time to wait for the next message. + * @return the next message, or <tt>null</tt> if the timeout elapses without a + * message becoming available. + */ + public Message nextMessage(long timeout) { + return (Message)messageCollector.nextResult(timeout); + } + + /** + * Adds a packet listener that will be notified of any new messages in the + * group chat. Only "group chat" messages addressed to this group chat will + * be delivered to the listener. If you wish to listen for other packets + * that may be associated with this group chat, you should register a + * PacketListener directly with the XMPPConnection with the appropriate + * PacketListener. + * + * @param listener a packet listener. + */ + public void addMessageListener(PacketListener listener) { + connection.addPacketListener(listener, messageFilter); + connectionListeners.add(listener); + } + + public void finalize() throws Throwable { + super.finalize(); + try { + if (messageCollector != null) { + messageCollector.cancel(); + } + // Remove all the PacketListeners added to the connection by this GroupChat + for (Iterator it=connectionListeners.iterator(); it.hasNext();) { + connection.removePacketListener((PacketListener) it.next()); + } + } + catch (Exception e) {} + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/NonSASLAuthentication.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/NonSASLAuthentication.java new file mode 100644 index 000000000..d392f0509 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/NonSASLAuthentication.java @@ -0,0 +1,128 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.Authentication; +import org.jivesoftware.smack.packet.IQ; + +/** + * Implementation of JEP-0078: Non-SASL Authentication. Follow the following + * <a href=http://www.jabber.org/jeps/jep-0078.html>link</a> to obtain more + * information about the JEP. + * + * @author Gaston Dombiak + */ +class NonSASLAuthentication implements UserAuthentication { + + private XMPPConnection connection; + + public NonSASLAuthentication(XMPPConnection connection) { + super(); + this.connection = connection; + } + + public String authenticate(String username, String password, String resource) throws + XMPPException { + // If we send an authentication packet in "get" mode with just the username, + // the server will return the list of authentication protocols it supports. + Authentication discoveryAuth = new Authentication(); + discoveryAuth.setType(IQ.Type.GET); + discoveryAuth.setUsername(username); + + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(discoveryAuth.getPacketID())); + // Send the packet + connection.sendPacket(discoveryAuth); + // Wait up to a certain number of seconds for a response from the server. + IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + if (response == null) { + throw new XMPPException("No response from the server."); + } + // If the server replied with an error, throw an exception. + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + // Otherwise, no error so continue processing. + Authentication authTypes = (Authentication) response; + collector.cancel(); + + // Now, create the authentication packet we'll send to the server. + Authentication auth = new Authentication(); + auth.setUsername(username); + + // Figure out if we should use digest or plain text authentication. + if (authTypes.getDigest() != null) { + auth.setDigest(connection.getConnectionID(), password); + } + else if (authTypes.getPassword() != null) { + auth.setPassword(password); + } + else { + throw new XMPPException("Server does not support compatible authentication mechanism."); + } + + auth.setResource(resource); + + collector = connection.createPacketCollector(new PacketIDFilter(auth.getPacketID())); + // Send the packet. + connection.sendPacket(auth); + // Wait up to a certain number of seconds for a response from the server. + response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + if (response == null) { + throw new XMPPException("Authentication failed."); + } + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + // We're done with the collector, so explicitly cancel it. + collector.cancel(); + + return response.getTo(); + } + + public String authenticateAnonymously() throws XMPPException { + // Create the authentication packet we'll send to the server. + Authentication auth = new Authentication(); + + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(auth.getPacketID())); + // Send the packet. + connection.sendPacket(auth); + // Wait up to a certain number of seconds for a response from the server. + IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + if (response == null) { + throw new XMPPException("Anonymous login failed."); + } + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + // We're done with the collector, so explicitly cancel it. + collector.cancel(); + + if (response.getTo() != null) { + return response.getTo(); + } + else { + return connection.serviceName + "/" + ((Authentication) response).getResource(); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/OpenTrustManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/OpenTrustManager.java new file mode 100644 index 000000000..3d30a22df --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/OpenTrustManager.java @@ -0,0 +1,49 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import javax.net.ssl.X509TrustManager; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * Dummy trust manager that trust all certificates presented by the server. This class + * is used during old SSL connections. + * + * @author Gaston Dombiak + */ +class OpenTrustManager implements X509TrustManager { + + public OpenTrustManager() { + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(X509Certificate[] arg0, String arg1) + throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] arg0, String arg1) + throws CertificateException { + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketCollector.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketCollector.java new file mode 100644 index 000000000..b54cc82dc --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketCollector.java @@ -0,0 +1,185 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.filter.PacketFilter; + +import java.util.LinkedList; + +/** + * Provides a mechanism to collect packets into a result queue that pass a + * specified filter. The collector lets you perform blocking and polling + * operations on the result queue. So, a PacketCollector is more suitable to + * use than a {@link PacketListener} when you need to wait for a specific + * result.<p> + * + * Each packet collector will queue up to 2^16 packets for processing before + * older packets are automatically dropped. + * + * @see XMPPConnection#createPacketCollector(PacketFilter) + * @author Matt Tucker + */ +public class PacketCollector { + + /** + * Max number of packets that any one collector can hold. After the max is + * reached, older packets will be automatically dropped from the queue as + * new packets are added. + */ + private static final int MAX_PACKETS = 65536; + + private PacketFilter packetFilter; + private LinkedList resultQueue; + private PacketReader packetReader; + private boolean cancelled = false; + + /** + * Creates a new packet collector. If the packet filter is <tt>null</tt>, then + * all packets will match this collector. + * + * @param packetReader the packetReader the collector is tied to. + * @param packetFilter determines which packets will be returned by this collector. + */ + protected PacketCollector(PacketReader packetReader, PacketFilter packetFilter) { + this.packetReader = packetReader; + this.packetFilter = packetFilter; + this.resultQueue = new LinkedList(); + // Add the collector to the packet reader's list of active collector. + synchronized (packetReader.collectors) { + packetReader.collectors.add(this); + } + } + + /** + * Explicitly cancels the packet collector so that no more results are + * queued up. Once a packet collector has been cancelled, it cannot be + * re-enabled. Instead, a new packet collector must be created. + */ + public void cancel() { + // If the packet collector has already been cancelled, do nothing. + if (!cancelled) { + cancelled = true; + // Remove object from collectors list by setting the value in the + // list at the correct index to null. The collector thread will + // automatically remove the actual list entry when it can. + synchronized (packetReader.collectors) { + int index = packetReader.collectors.indexOf(this); + packetReader.collectors.set(index, null); + } + } + } + + /** + * Returns the packet filter associated with this packet collector. The packet + * filter is used to determine what packets are queued as results. + * + * @return the packet filter. + */ + public PacketFilter getPacketFilter() { + return packetFilter; + } + + /** + * Polls to see if a packet is currently available and returns it, or + * immediately returns <tt>null</tt> if no packets are currently in the + * result queue. + * + * @return the next packet result, or <tt>null</tt> if there are no more + * results. + */ + public synchronized Packet pollResult() { + if (resultQueue.isEmpty()) { + return null; + } + else { + return (Packet)resultQueue.removeLast(); + } + } + + /** + * Returns the next available packet. The method call will block (not return) + * until a packet is available. + * + * @return the next available packet. + */ + public synchronized Packet nextResult() { + // Wait indefinitely until there is a result to return. + while (resultQueue.isEmpty()) { + try { + wait(); + } + catch (InterruptedException ie) { + // Ignore. + } + } + return (Packet)resultQueue.removeLast(); + } + + /** + * Returns the next available packet. The method call will block (not return) + * until a packet is available or the <tt>timeout</tt> has elapased. If the + * timeout elapses without a result, <tt>null</tt> will be returned. + * + * @param timeout the amount of time to wait for the next packet (in milleseconds). + * @return the next available packet. + */ + public synchronized Packet nextResult(long timeout) { + // Wait up to the specified amount of time for a result. + if (resultQueue.isEmpty()) { + try { + wait(timeout); + } + catch (InterruptedException ie) { + // Ignore. + } + } + // If still no result, return null. + if (resultQueue.isEmpty()) { + return null; + } + else { + return (Packet)resultQueue.removeLast(); + } + } + + /** + * Processes a packet to see if it meets the criteria for this packet collector. + * If so, the packet is added to the result queue. + * + * @param packet the packet to process. + */ + protected synchronized void processPacket(Packet packet) { + if (packet == null) { + return; + } + if (packetFilter == null || packetFilter.accept(packet)) { + // If the max number of packets has been reached, remove the oldest one. + if (resultQueue.size() == MAX_PACKETS) { + resultQueue.removeLast(); + } + // Add the new packet. + resultQueue.addFirst(packet); + // Notify waiting threads a result is available. + notifyAll(); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketInterceptor.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketInterceptor.java new file mode 100644 index 000000000..849c3fb38 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketInterceptor.java @@ -0,0 +1,49 @@ +/** + * $Revision$ + * $Date$ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.packet.Packet; + +/** + * Provides a mechanism to intercept and modify packets that are going to be + * sent to the server. PacketInterceptors are added to the {@link XMPPConnection} + * together with a {@link org.jivesoftware.smack.filter.PacketFilter} so that only + * certain packets are intercepted and processed by the interceptor.<p> + * + * This allows event-style programming -- every time a new packet is found, + * the {@link #interceptPacket(Packet)} method will be called. + * + * @see XMPPConnection#addPacketWriterInterceptor(PacketInterceptor, org.jivesoftware.smack.filter.PacketFilter) + * @author Gaston Dombiak + */ +public interface PacketInterceptor { + + /** + * Process the packet that is about to be sent to the server. The intercepted + * packet can be modified by the interceptor.<p> + * + * Interceptors are invoked using the same thread that requested the packet + * to be sent, so it's very important that implementations of this method + * not block for any extended period of time. + * + * @param packet the packet to is going to be sent to the server. + */ + public void interceptPacket(Packet packet); +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketListener.java new file mode 100644 index 000000000..77c6deabe --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketListener.java @@ -0,0 +1,48 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.packet.Packet; + +/** + * Provides a mechanism to listen for packets that pass a specified filter. + * This allows event-style programming -- every time a new packet is found, + * the {@link #processPacket(Packet)} method will be called. This is the + * opposite approach to the functionality provided by a {@link PacketCollector} + * which lets you block while waiting for results. + * + * @see XMPPConnection#addPacketListener(PacketListener, org.jivesoftware.smack.filter.PacketFilter) + * @author Matt Tucker + */ +public interface PacketListener { + + /** + * Process the next packet sent to this packet listener.<p> + * + * A single thread is responsible for invoking all listeners, so + * it's very important that implementations of this method not block + * for any extended period of time. + * + * @param packet the packet to process. + */ + public void processPacket(Packet packet); + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketReader.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketReader.java new file mode 100644 index 000000000..946468262 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketReader.java @@ -0,0 +1,854 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.xmlpull.mxp1.MXParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.*; + +/** + * Listens for XML traffic from the XMPP server and parses it into packet objects. + * The packet reader also manages all packet listeners and collectors.<p> + * + * @see PacketCollector + * @see PacketListener + * @author Matt Tucker + */ +class PacketReader { + + private Thread readerThread; + private Thread listenerThread; + + private XMPPConnection connection; + private XmlPullParser parser; + private boolean done = false; + protected List collectors = new ArrayList(); + private List listeners = new ArrayList(); + protected List connectionListeners = new ArrayList(); + + private String connectionID = null; + private Object connectionIDLock = new Object(); + + protected PacketReader(XMPPConnection connection) { + this.connection = connection; + + readerThread = new Thread() { + public void run() { + parsePackets(); + } + }; + readerThread.setName("Smack Packet Reader"); + readerThread.setDaemon(true); + + listenerThread = new Thread() { + public void run() { + try { + processListeners(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + }; + listenerThread.setName("Smack Listener Processor"); + listenerThread.setDaemon(true); + + resetParser(); + } + + /** + * Creates a new packet collector for this reader. A packet filter determines + * which packets will be accumulated by the collector. + * + * @param packetFilter the packet filter to use. + * @return a new packet collector. + */ + public PacketCollector createPacketCollector(PacketFilter packetFilter) { + return new PacketCollector(this, packetFilter); + } + + /** + * Registers a packet listener with this reader. A packet filter determines + * which packets will be delivered to the listener. + * + * @param packetListener the packet listener to notify of new packets. + * @param packetFilter the packet filter to use. + */ + public void addPacketListener(PacketListener packetListener, PacketFilter packetFilter) { + ListenerWrapper wrapper = new ListenerWrapper(this, packetListener, + packetFilter); + synchronized (listeners) { + listeners.add(wrapper); + } + } + + /** + * Removes a packet listener. + * + * @param packetListener the packet listener to remove. + */ + public void removePacketListener(PacketListener packetListener) { + synchronized (listeners) { + for (int i=0; i<listeners.size(); i++) { + ListenerWrapper wrapper = (ListenerWrapper)listeners.get(i); + if (wrapper != null && wrapper.packetListener.equals(packetListener)) { + listeners.set(i, null); + } + } + } + } + + /** + * Starts the packet reader thread and returns once a connection to the server + * has been established. A connection will be attempted for a maximum of five + * seconds. An XMPPException will be thrown if the connection fails. + * + * @throws XMPPException if the server fails to send an opening stream back + * for more than five seconds. + */ + public void startup() throws XMPPException { + readerThread.start(); + listenerThread.start(); + // Wait for stream tag before returing. We'll wait a couple of seconds before + // giving up and throwing an error. + try { + synchronized(connectionIDLock) { + if (connectionID == null) { + // A waiting thread may be woken up before the wait time or a notify + // (although this is a rare thing). Therefore, we continue waiting + // until either a connectionID has been set (and hence a notify was + // made) or the total wait time has elapsed. + long waitTime = SmackConfiguration.getPacketReplyTimeout(); + long start = System.currentTimeMillis(); + while (connectionID == null && !done) { + if (waitTime <= 0) { + break; + } + // Wait 3 times the standard time since TLS may take a while + connectionIDLock.wait(waitTime * 3); + long now = System.currentTimeMillis(); + waitTime -= now - start; + start = now; + } + } + } + } + catch (InterruptedException ie) { + // Ignore. + } + if (connectionID == null) { + throw new XMPPException("Connection failed. No response from server."); + } + else { + connection.connectionID = connectionID; + } + } + + /** + * Shuts the packet reader down. + */ + public void shutdown() { + // Notify connection listeners of the connection closing if done hasn't already been set. + if (!done) { + ArrayList listenersCopy; + synchronized (connectionListeners) { + // Make a copy since it's possible that a listener will be removed from the list + listenersCopy = new ArrayList(connectionListeners); + for (Iterator i=listenersCopy.iterator(); i.hasNext(); ) { + ConnectionListener listener = (ConnectionListener)i.next(); + listener.connectionClosed(); + } + } + } + done = true; + + // Make sure that the listenerThread is awake to shutdown properly + synchronized (listenerThread) { + listenerThread.notify(); + } + } + + /** + * Sends out a notification that there was an error with the connection + * and closes the connection. + * + * @param e the exception that causes the connection close event. + */ + void notifyConnectionError(Exception e) { + done = true; + connection.close(); + // Print the stack trace to help catch the problem + e.printStackTrace(); + // Notify connection listeners of the error. + ArrayList listenersCopy; + synchronized (connectionListeners) { + // Make a copy since it's possible that a listener will be removed from the list + listenersCopy = new ArrayList(connectionListeners); + for (Iterator i=listenersCopy.iterator(); i.hasNext(); ) { + ConnectionListener listener = (ConnectionListener)i.next(); + listener.connectionClosedOnError(e); + } + } + } + + /** + * Resets the parser using the latest connection's reader. Reseting the parser is necessary + * when the plain connection has been secured or when a new opening stream element is going + * to be sent by the server. + */ + private void resetParser() { + try { + parser = new MXParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(connection.reader); + } + catch (XmlPullParserException xppe) { + xppe.printStackTrace(); + } + } + + /** + * Process listeners. + */ + private void processListeners() { + while (!done) { + synchronized (listeners) { + if (listeners.size() > 0) { + for (int i=listeners.size()-1; i>=0; i--) { + if (listeners.get(i) == null) { + listeners.remove(i); + } + } + } + } + boolean processedPacket = false; + int size = listeners.size(); + for (int i=0; i<size; i++) { + ListenerWrapper wrapper = (ListenerWrapper)listeners.get(i); + if (wrapper != null) { + processedPacket = processedPacket || wrapper.notifyListener(); + } + } + if (!processedPacket) { + try { + // Wait until more packets are ready to be processed. + synchronized (listenerThread) { + listenerThread.wait(); + } + } + catch (InterruptedException ie) { + // Ignore. + } + } + } + } + + /** + * Parse top-level packets in order to process them further. + */ + private void parsePackets() { + try { + int eventType = parser.getEventType(); + do { + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("message")) { + processPacket(PacketParserUtils.parseMessage(parser)); + } + else if (parser.getName().equals("iq")) { + processPacket(parseIQ(parser)); + } + else if (parser.getName().equals("presence")) { + processPacket(PacketParserUtils.parsePresence(parser)); + } + // We found an opening stream. Record information about it, then notify + // the connectionID lock so that the packet reader startup can finish. + else if (parser.getName().equals("stream")) { + // Ensure the correct jabber:client namespace is being used. + if ("jabber:client".equals(parser.getNamespace(null))) { + // Get the connection id. + for (int i=0; i<parser.getAttributeCount(); i++) { + if (parser.getAttributeName(i).equals("id")) { + // Save the connectionID + connectionID = parser.getAttributeValue(i); + if (!"1.0".equals(parser.getAttributeValue("", "version"))) { + // Notify that a stream has been opened if the + // server is not XMPP 1.0 compliant otherwise make the + // notification after TLS has been negotiated or if TLS + // is not supported + releaseConnectionIDLock(); + } + } + else if (parser.getAttributeName(i).equals("from")) { + // Use the server name that the server says that it is. + connection.serviceName = parser.getAttributeValue(i); + } + } + } + } + else if (parser.getName().equals("error")) { + throw new XMPPException(parseStreamError(parser)); + } + else if (parser.getName().equals("features")) { + parseFeatures(parser); + } + else if (parser.getName().equals("proceed")) { + // Secure the connection by negotiating TLS + connection.proceedTLSReceived(); + // Reset the state of the parser since a new stream element is going + // to be sent by the server + resetParser(); + } + else if (parser.getName().equals("failure")) { + String namespace = parser.getNamespace(null); + if ("urn:ietf:params:xml:ns:xmpp-tls".equals(namespace)) { + // TLS negotiation has failed. The server will close the connection + throw new Exception("TLS negotiation has failed"); + } + else if ("http://jabber.org/protocol/compress".equals(namespace)) { + // Stream compression has been denied. This is a recoverable + // situation. It is still possible to authenticate and + // use the connection but using an uncompressed connection + connection.streamCompressionDenied(); + } + else { + // SASL authentication has failed. The server may close the connection + // depending on the number of retries + connection.getSASLAuthentication().authenticationFailed(); + } + } + else if (parser.getName().equals("challenge")) { + // The server is challenging the SASL authentication made by the client + connection.getSASLAuthentication().challengeReceived(parser.nextText()); + } + else if (parser.getName().equals("success")) { + // We now need to bind a resource for the connection + // Open a new stream and wait for the response + connection.packetWriter.openStream(); + + // Reset the state of the parser since a new stream element is going + // to be sent by the server + resetParser(); + + // The SASL authentication with the server was successful. The next step + // will be to bind the resource + connection.getSASLAuthentication().authenticated(); + } + else if (parser.getName().equals("compressed")) { + // Server confirmed that it's possible to use stream compression. Start + // stream compression + connection.startStreamCompression(); + // Reset the state of the parser since a new stream element is going + // to be sent by the server + resetParser(); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("stream")) { + // Close the connection. + connection.close(); + } + } + eventType = parser.next(); + } while (!done && eventType != XmlPullParser.END_DOCUMENT); + } + catch (Exception e) { + if (!done) { + // Close the connection and notify connection listeners of the + // error. + notifyConnectionError(e); + } + } + } + + /** + * Releases the connection ID lock so that the thread that was waiting can resume. The + * lock will be released when one of the following three conditions is met:<p> + * + * 1) An opening stream was sent from a non XMPP 1.0 compliant server + * 2) Stream features were received from an XMPP 1.0 compliant server that does not support TLS + * 3) TLS negotiation was successful + * + */ + private void releaseConnectionIDLock() { + synchronized(connectionIDLock) { + connectionIDLock.notifyAll(); + } + } + + /** + * Processes a packet after it's been fully parsed by looping through the installed + * packet collectors and listeners and letting them examine the packet to see if + * they are a match with the filter. + * + * @param packet the packet to process. + */ + private void processPacket(Packet packet) { + if (packet == null) { + return; + } + + // Remove all null values from the collectors list. + synchronized (collectors) { + for (int i=collectors.size()-1; i>=0; i--) { + if (collectors.get(i) == null) { + collectors.remove(i); + } + } + } + + // Loop through all collectors and notify the appropriate ones. + int size = collectors.size(); + for (int i=0; i<size; i++) { + PacketCollector collector = (PacketCollector)collectors.get(i); + if (collector != null) { + // Have the collector process the packet to see if it wants to handle it. + collector.processPacket(packet); + } + } + + // Notify the listener thread that packets are waiting. + synchronized (listenerThread) { + listenerThread.notifyAll(); + } + } + + private StreamError parseStreamError(XmlPullParser parser) throws IOException, + XmlPullParserException { + StreamError streamError = null; + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + streamError = new StreamError(parser.getName()); + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("error")) { + done = true; + } + } + } + return streamError; + } + + private void parseFeatures(XmlPullParser parser) throws Exception { + boolean startTLSReceived = false; + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("starttls")) { + startTLSReceived = true; + // Confirm the server that we want to use TLS + connection.startTLSReceived(); + } + else if (parser.getName().equals("mechanisms")) { + // The server is reporting available SASL mechanisms. Store this information + // which will be used later while logging (i.e. authenticating) into + // the server + connection.getSASLAuthentication() + .setAvailableSASLMethods(parseMechanisms(parser)); + } + else if (parser.getName().equals("bind")) { + // The server requires the client to bind a resource to the stream + connection.getSASLAuthentication().bindingRequired(); + } + else if (parser.getName().equals("session")) { + // The server supports sessions + connection.getSASLAuthentication().sessionsSupported(); + } + else if (parser.getName().equals("compression")) { + // The server supports stream compression + connection.setAvailableCompressionMethods(parseCompressionMethods(parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("features")) { + done = true; + } + } + } + if (!startTLSReceived) { + releaseConnectionIDLock(); + } + } + /** + * Sends out a notification that there was an error with the connection and + * closes the connection. + * + * @param e + * the exception that causes the connection close event. + */ + void notifyConnectionAuthenticated() { + ArrayList listenersCopy; + synchronized (connectionListeners) { + // Make a copy since it's possible that a listener will be removed + // from the list + listenersCopy = new ArrayList(connectionListeners); + for (Iterator i = listenersCopy.iterator(); i.hasNext();) { + ConnectionListener listener = (ConnectionListener) i.next(); + if (listener instanceof ConnectionListener2) + ((ConnectionListener2) listener).connectionAuthenticated(); + } + } + } + /** + * Returns a collection of Stings with the mechanisms included in the + * mechanisms stanza. + * + * @param parser + * the XML parser, positioned at the start of an IQ packet. + * @return a collection of Stings with the mechanisms included in the + * mechanisms stanza. + * @throws Exception + * if an exception occurs while parsing the stanza. + */ + private Collection parseMechanisms(XmlPullParser parser) throws Exception { + List mechanisms = new ArrayList(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + if (elementName.equals("mechanism")) { + mechanisms.add(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("mechanisms")) { + done = true; + } + } + } + return mechanisms; + } + + private Collection parseCompressionMethods(XmlPullParser parser) + throws IOException, XmlPullParserException { + List methods = new ArrayList(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + if (elementName.equals("method")) { + methods.add(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("compression")) { + done = true; + } + } + } + return methods; + } + + /** + * Parses an IQ packet. + * + * @param parser the XML parser, positioned at the start of an IQ packet. + * @return an IQ object. + * @throws Exception if an exception occurs while parsing the packet. + */ + private IQ parseIQ(XmlPullParser parser) throws Exception { + IQ iqPacket = null; + + String id = parser.getAttributeValue("", "id"); + String to = parser.getAttributeValue("", "to"); + String from = parser.getAttributeValue("", "from"); + IQ.Type type = IQ.Type.fromString(parser.getAttributeValue("", "type")); + XMPPError error = null; + + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + if (elementName.equals("error")) { + error = PacketParserUtils.parseError(parser); + } + else if (elementName.equals("query") && namespace.equals("jabber:iq:auth")) { + iqPacket = parseAuthentication(parser); + } + else if (elementName.equals("query") && namespace.equals("jabber:iq:roster")) { + iqPacket = parseRoster(parser); + } + else if (elementName.equals("query") && namespace.equals("jabber:iq:register")) { + iqPacket = parseRegistration(parser); + } + else if (elementName.equals("bind") && + namespace.equals("urn:ietf:params:xml:ns:xmpp-bind")) { + iqPacket = parseResourceBinding(parser); + } + // Otherwise, see if there is a registered provider for + // this element name and namespace. + else { + Object provider = ProviderManager.getDefault().getIQProvider(elementName, namespace); + if (provider != null) { + if (provider instanceof IQProvider) { + iqPacket = ((IQProvider)provider).parseIQ(parser); + } + else if (provider instanceof Class) { + iqPacket = (IQ)PacketParserUtils.parseWithIntrospection(elementName, + (Class)provider, parser); + } + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("iq")) { + done = true; + } + } + } + // Decide what to do when an IQ packet was not understood + if (iqPacket == null) { + if (IQ.Type.GET == type || IQ.Type.SET == type ) { + // If the IQ stanza is of type "get" or "set" containing a child element + // qualified by a namespace it does not understand, then answer an IQ of + // type "error" with code 501 ("feature-not-implemented") + iqPacket = new IQ() { + public String getChildElementXML() { + return null; + } + }; + iqPacket.setPacketID(id); + iqPacket.setTo(from); + iqPacket.setFrom(to); + iqPacket.setType(IQ.Type.ERROR); + iqPacket.setError(new XMPPError(501, "feature-not-implemented")); + connection.sendPacket(iqPacket); + return null; + } + else { + // If an IQ packet wasn't created above, create an empty IQ packet. + iqPacket = new IQ() { + public String getChildElementXML() { + return null; + } + }; + } + } + + // Set basic values on the iq packet. + iqPacket.setPacketID(id); + iqPacket.setTo(to); + iqPacket.setFrom(from); + iqPacket.setType(type); + iqPacket.setError(error); + + return iqPacket; + } + + private Authentication parseAuthentication(XmlPullParser parser) throws Exception { + Authentication authentication = new Authentication(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("username")) { + authentication.setUsername(parser.nextText()); + } + else if (parser.getName().equals("password")) { + authentication.setPassword(parser.nextText()); + } + else if (parser.getName().equals("digest")) { + authentication.setDigest(parser.nextText()); + } + else if (parser.getName().equals("resource")) { + authentication.setResource(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + return authentication; + } + + private RosterPacket parseRoster(XmlPullParser parser) throws Exception { + RosterPacket roster = new RosterPacket(); + boolean done = false; + RosterPacket.Item item = null; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + String jid = parser.getAttributeValue("", "jid"); + String name = parser.getAttributeValue("", "name"); + // Create packet. + item = new RosterPacket.Item(jid, name); + // Set status. + String ask = parser.getAttributeValue("", "ask"); + RosterPacket.ItemStatus status = RosterPacket.ItemStatus.fromString(ask); + item.setItemStatus(status); + // Set type. + String subscription = parser.getAttributeValue("", "subscription"); + RosterPacket.ItemType type = RosterPacket.ItemType.fromString(subscription); + item.setItemType(type); + } + if (parser.getName().equals("group") && item!= null) { + item.addGroupName(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + roster.addRosterItem(item); + } + if (parser.getName().equals("query")) { + done = true; + } + } + } + return roster; + } + + private Registration parseRegistration(XmlPullParser parser) throws Exception { + Registration registration = new Registration(); + Map fields = null; + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + // Any element that's in the jabber:iq:register namespace, + // attempt to parse it if it's in the form <name>value</name>. + if (parser.getNamespace().equals("jabber:iq:register")) { + String name = parser.getName(); + String value = ""; + if (fields == null) { + fields = new HashMap(); + } + + if (parser.next() == XmlPullParser.TEXT) { + value = parser.getText(); + } + // Ignore instructions, but anything else should be added to the map. + if (!name.equals("instructions")) { + fields.put(name, value); + } + else { + registration.setInstructions(value); + } +} + // Otherwise, it must be a packet extension. + else { + registration.addExtension( + PacketParserUtils.parsePacketExtension( + parser.getName(), + parser.getNamespace(), + parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + registration.setAttributes(fields); + return registration; + } + + private Bind parseResourceBinding(XmlPullParser parser) throws IOException, + XmlPullParserException { + Bind bind = new Bind(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("resource")) { + bind.setResource(parser.nextText()); + } + else if (parser.getName().equals("jid")) { + bind.setJid(parser.nextText()); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("bind")) { + done = true; + } + } + } + + return bind; + } + + /** + * A wrapper class to associate a packet collector with a listener. + */ + private static class ListenerWrapper { + + private PacketListener packetListener; + private PacketCollector packetCollector; + + public ListenerWrapper(PacketReader packetReader, PacketListener packetListener, + PacketFilter packetFilter) + { + this.packetListener = packetListener; + this.packetCollector = new PacketCollector(packetReader, packetFilter); + } + + public boolean equals(Object object) { + if (object == null) { + return false; + } + if (object instanceof ListenerWrapper) { + return ((ListenerWrapper)object).packetListener.equals(this.packetListener); + } + else if (object instanceof PacketListener) { + return object.equals(this.packetListener); + } + return false; + } + + public boolean notifyListener() { + Packet packet = packetCollector.pollResult(); + if (packet != null) { + packetListener.processPacket(packet); + return true; + } + else { + return false; + } + } + + public void cancel() { + packetCollector.cancel(); + packetCollector = null; + packetListener = null; + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketWriter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketWriter.java new file mode 100644 index 000000000..cbfc6be53 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/PacketWriter.java @@ -0,0 +1,451 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import java.util.*; +import java.io.*; + +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Packet; + +/** + * Writes packets to a XMPP server. Packets are sent using a dedicated thread. Packet + * interceptors can be registered to dynamically modify packets before they're actually + * sent. Packet listeners can be registered to listen for all outgoing packets. + * + * @author Matt Tucker + */ +class PacketWriter { + + private Thread writerThread; + private Writer writer; + private XMPPConnection connection; + private LinkedList queue; + private boolean done = false; + + private List listeners = new ArrayList(); + private boolean listenersDeleted = false; + + /** + * List of PacketInterceptor that will be notified when a new packet is about to be + * sent to the server. These interceptors may modify the packet before it is being + * actually sent to the server. + */ + private List interceptors = new ArrayList(); + /** + * Flag that indicates if an interceptor was deleted. This is an optimization flag. + */ + private boolean interceptorDeleted = false; + + /** + * Creates a new packet writer with the specified connection. + * + * @param connection the connection. + */ + protected PacketWriter(XMPPConnection connection) { + this.connection = connection; + this.writer = connection.writer; + this.queue = new LinkedList(); + + writerThread = new Thread() { + public void run() { + writePackets(); + } + }; + writerThread.setName("Smack Packet Writer"); + writerThread.setDaemon(true); + + // Schedule a keep-alive task to run if the feature is enabled. will write + // out a space character each time it runs to keep the TCP/IP connection open. + int keepAliveInterval = SmackConfiguration.getKeepAliveInterval(); + if (keepAliveInterval > 0) { + Thread keepAliveThread = new Thread(new KeepAliveTask(keepAliveInterval)); + keepAliveThread.setDaemon(true); + keepAliveThread.start(); + } + } + + /** + * Sends the specified packet to the server. + * + * @param packet the packet to send. + */ + public void sendPacket(Packet packet) { + if (!done) { + // Invoke interceptors for the new packet that is about to be sent. Interceptors + // may modify the content of the packet. + processInterceptors(packet); + + synchronized(queue) { + queue.addFirst(packet); + queue.notifyAll(); + } + + // Process packet writer listeners. Note that we're using the sending + // thread so it's expected that listeners are fast. + processListeners(packet); + } + } + + /** + * Registers a packet listener with this writer. The listener will be + * notified immediately after every packet this writer sends. A packet filter + * determines which packets will be delivered to the listener. Note that the thread + * that writes packets will be used to invoke the listeners. Therefore, each + * packet listener should complete all operations quickly or use a different + * thread for processing. + * + * @param packetListener the packet listener to notify of sent packets. + * @param packetFilter the packet filter to use. + */ + public void addPacketListener(PacketListener packetListener, PacketFilter packetFilter) { + synchronized (listeners) { + listeners.add(new ListenerWrapper(packetListener, packetFilter)); + } + } + + /** + * Removes a packet listener. + * + * @param packetListener the packet listener to remove. + */ + public void removePacketListener(PacketListener packetListener) { + synchronized (listeners) { + for (int i=0; i<listeners.size(); i++) { + ListenerWrapper wrapper = (ListenerWrapper)listeners.get(i); + if (wrapper != null && wrapper.packetListener.equals(packetListener)) { + listeners.set(i, null); + // Set the flag to indicate that the listener list needs + // to be cleaned up. + listenersDeleted = true; + } + } + } + } + + /** + * Returns the number of registered packet listeners. + * + * @return the count of packet listeners. + */ + public int getPacketListenerCount() { + synchronized (listeners) { + return listeners.size(); + } + } + + /** + * Registers a packet interceptor with this writer. The interceptor will be + * notified of every packet that this writer is about to send. Interceptors + * may modify the packet to be sent. A packet filter determines which packets + * will be delivered to the interceptor. + * + * @param packetInterceptor the packet interceptor to notify of packets about to be sent. + * @param packetFilter the packet filter to use. + */ + public void addPacketInterceptor(PacketInterceptor packetInterceptor, PacketFilter packetFilter) { + synchronized (interceptors) { + interceptors.add(new InterceptorWrapper(packetInterceptor, packetFilter)); + } + } + + /** + * Removes a packet interceptor. + * + * @param packetInterceptor the packet interceptor to remove. + */ + public void removePacketInterceptor(PacketInterceptor packetInterceptor) { + synchronized (interceptors) { + for (int i=0; i<interceptors.size(); i++) { + InterceptorWrapper wrapper = (InterceptorWrapper)interceptors.get(i); + if (wrapper != null && wrapper.packetInterceptor.equals(packetInterceptor)) { + interceptors.set(i, null); + // Set the flag to indicate that the interceptor list needs + // to be cleaned up. + interceptorDeleted = true; + } + } + } + } + + /** + * Starts the packet writer thread and opens a connection to the server. The + * packet writer will continue writing packets until {@link #shutdown} or an + * error occurs. + */ + public void startup() { + writerThread.start(); + } + + void setWriter(Writer writer) { + this.writer = writer; + } + + /** + * Shuts down the packet writer. Once this method has been called, no further + * packets will be written to the server. + */ + public void shutdown() { + done = true; + } + + /** + * Returns the next available packet from the queue for writing. + * + * @return the next packet for writing. + */ + private Packet nextPacket() { + synchronized(queue) { + while (!done && queue.size() == 0) { + try { + queue.wait(2000); + } + catch (InterruptedException ie) { } + } + if (queue.size() > 0) { + return (Packet)queue.removeLast(); + } + else { + return null; + } + } + } + + private void writePackets() { + try { + // Open the stream. + openStream(); + // Write out packets from the queue. + while (!done) { + Packet packet = nextPacket(); + if (packet != null) { + synchronized (writer) { + writer.write(packet.toXML()); + writer.flush(); + } + } + } + // Close the stream. + try { + writer.write("</stream:stream>"); + writer.flush(); + } + catch (Exception e) { } + finally { + try { + writer.close(); + } + catch (Exception e) { } + } + } + catch (IOException ioe){ + if (!done) { + done = true; + connection.packetReader.notifyConnectionError(ioe); + } + } + } + + /** + * Process listeners. + */ + private void processListeners(Packet packet) { + // Clean up null entries in the listeners list if the flag is set. List + // removes are done seperately so that the main notification process doesn't + // need to synchronize on the list. + synchronized (listeners) { + if (listenersDeleted) { + for (int i=listeners.size()-1; i>=0; i--) { + if (listeners.get(i) == null) { + listeners.remove(i); + } + } + listenersDeleted = false; + } + } + // Notify the listeners of the new sent packet + int size = listeners.size(); + for (int i=0; i<size; i++) { + ListenerWrapper listenerWrapper = (ListenerWrapper)listeners.get(i); + if (listenerWrapper != null) { + listenerWrapper.notifyListener(packet); + } + } + } + + /** + * Process interceptors. Interceptors may modify the packet that is about to be sent. + * Since the thread that requested to send the packet will invoke all interceptors, it + * is important that interceptors perform their work as soon as possible so that the + * thread does not remain blocked for a long period. + * + * @param packet the packet that is going to be sent to the server + */ + private void processInterceptors(Packet packet) { + if (packet != null) { + // Clean up null entries in the interceptors list if the flag is set. List + // removes are done seperately so that the main notification process doesn't + // need to synchronize on the list. + synchronized (interceptors) { + if (interceptorDeleted) { + for (int i=interceptors.size()-1; i>=0; i--) { + if (interceptors.get(i) == null) { + interceptors.remove(i); + } + } + interceptorDeleted = false; + } + } + // Notify the interceptors of the new packet to be sent + int size = interceptors.size(); + for (int i=0; i<size; i++) { + InterceptorWrapper interceptorWrapper = (InterceptorWrapper)interceptors.get(i); + if (interceptorWrapper != null) { + interceptorWrapper.notifyListener(packet); + } + } + } + } + + /** + * Sends to the server a new stream element. This operation may be requested several times + * so we need to encapsulate the logic in one place. This message will be sent while doing + * TLS, SASL and resource binding. + * + * @throws IOException If an error occurs while sending the stanza to the server. + */ + void openStream() throws IOException { + StringBuffer stream = new StringBuffer(); + stream.append("<stream:stream"); + stream.append(" to=\"").append(connection.serviceName).append("\""); + stream.append(" xmlns=\"jabber:client\""); + stream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\""); + if (connection instanceof SSLXMPPConnection) { + // Old SSL connections should not include indicate XMPP 1.0 compliance + stream.append(">"); + } + else { + stream.append(" version=\"1.0\">"); + } + writer.write(stream.toString()); + writer.flush(); + } + + /** + * A wrapper class to associate a packet filter with a listener. + */ + private static class ListenerWrapper { + + private PacketListener packetListener; + private PacketFilter packetFilter; + + public ListenerWrapper(PacketListener packetListener, + PacketFilter packetFilter) + { + this.packetListener = packetListener; + this.packetFilter = packetFilter; + } + + public boolean equals(Object object) { + if (object == null) { + return false; + } + if (object instanceof ListenerWrapper) { + return ((ListenerWrapper)object).packetListener.equals(this.packetListener); + } + else if (object instanceof PacketListener) { + return object.equals(this.packetListener); + } + return false; + } + + public void notifyListener(Packet packet) { + if (packetFilter == null || packetFilter.accept(packet)) { + packetListener.processPacket(packet); + } + } + } + + /** + * A wrapper class to associate a packet filter with an interceptor. + */ + private static class InterceptorWrapper { + + private PacketInterceptor packetInterceptor; + private PacketFilter packetFilter; + + public InterceptorWrapper(PacketInterceptor packetInterceptor, PacketFilter packetFilter) + { + this.packetInterceptor = packetInterceptor; + this.packetFilter = packetFilter; + } + + public boolean equals(Object object) { + if (object == null) { + return false; + } + if (object instanceof InterceptorWrapper) { + return ((InterceptorWrapper) object).packetInterceptor + .equals(this.packetInterceptor); + } + else if (object instanceof PacketInterceptor) { + return object.equals(this.packetInterceptor); + } + return false; + } + + public void notifyListener(Packet packet) { + if (packetFilter == null || packetFilter.accept(packet)) { + packetInterceptor.interceptPacket(packet); + } + } + } + + /** + * A TimerTask that keeps connections to the server alive by sending a space + * character on an interval. + */ + private class KeepAliveTask implements Runnable { + + private int delay; + + public KeepAliveTask(int delay) { + this.delay = delay; + } + + public void run() { + while (!done) { + synchronized (writer) { + try { + writer.write(" "); + writer.flush(); + } + catch (Exception e) { } + } + try { + // Sleep until we should write the next keep-alive. + Thread.sleep(delay); + } + catch (InterruptedException ie) { } + } + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Roster.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Roster.java new file mode 100644 index 000000000..196a4c3ea --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/Roster.java @@ -0,0 +1,813 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smack.filter.*; +import org.jivesoftware.smack.util.StringUtils; + +import java.util.*; + +/** + * Represents a user's roster, which is the collection of users a person receives + * presence updates for. Roster items are categorized into groups for easier management.<p> + * + * Others users may attempt to subscribe to this user using a subscription request. Three + * modes are supported for handling these requests: <ul> + * <li> SUBSCRIPTION_ACCEPT_ALL -- accept all subscription requests. + * <li> SUBSCRIPTION_REJECT_ALL -- reject all subscription requests. + * <li> SUBSCRIPTION_MANUAL -- manually process all subscription requests. </ul> + * + * @see XMPPConnection#getRoster() + * @author Matt Tucker + */ +public class Roster { + + /** + * Automatically accept all subscription and unsubscription requests. This is + * the default mode and is suitable for simple client. More complex client will + * likely wish to handle subscription requests manually. + */ + public static final int SUBSCRIPTION_ACCEPT_ALL = 0; + + /** + * Automatically reject all subscription requests. + */ + public static final int SUBSCRIPTION_REJECT_ALL = 1; + + /** + * Subscription requests are ignored, which means they must be manually + * processed by registering a listener for presence packets and then looking + * for any presence requests that have the type Presence.Type.SUBSCRIBE or + * Presence.Type.UNSUBSCRIBE. + */ + public static final int SUBSCRIPTION_MANUAL = 2; + + /** + * The default subscription processing mode to use when a Roster is created. By default + * all subscription requests are automatically accepted. + */ + private static int defaultSubscriptionMode = SUBSCRIPTION_ACCEPT_ALL; + + private XMPPConnection connection; + private Map groups; + private List entries; + private List unfiledEntries; + private List rosterListeners; + private Map presenceMap; + // The roster is marked as initialized when at least a single roster packet + // has been recieved and processed. + boolean rosterInitialized = false; + + private int subscriptionMode = getDefaultSubscriptionMode(); + + /** + * Returns the default subscription processing mode to use when a new Roster is created. The + * subscription processing mode dictates what action Smack will take when subscription + * requests from other users are made. The default subscription mode + * is {@link #SUBSCRIPTION_ACCEPT_ALL}. + * + * @return the default subscription mode to use for new Rosters + */ + public static int getDefaultSubscriptionMode() { + return defaultSubscriptionMode; + } + + /** + * Sets the default subscription processing mode to use when a new Roster is created. The + * subscription processing mode dictates what action Smack will take when subscription + * requests from other users are made. The default subscription mode + * is {@link #SUBSCRIPTION_ACCEPT_ALL}. + * + * @param subscriptionMode the default subscription mode to use for new Rosters. + */ + public static void setDefaultSubscriptionMode(int subscriptionMode) { + defaultSubscriptionMode = subscriptionMode; + } + + /** + * Creates a new roster. + * + * @param connection an XMPP connection. + */ + Roster(final XMPPConnection connection) { + this.connection = connection; + groups = new Hashtable(); + unfiledEntries = new ArrayList(); + entries = new ArrayList(); + rosterListeners = new ArrayList(); + presenceMap = new HashMap(); + // Listen for any roster packets. + PacketFilter rosterFilter = new PacketTypeFilter(RosterPacket.class); + connection.addPacketListener(new RosterPacketListener(), rosterFilter); + // Listen for any presence packets. + PacketFilter presenceFilter = new PacketTypeFilter(Presence.class); + connection.addPacketListener(new PresencePacketListener(), presenceFilter); + } + + /** + * Returns the subscription processing mode, which dictates what action + * Smack will take when subscription requests from other users are made. + * The default subscription mode is {@link #SUBSCRIPTION_ACCEPT_ALL}.<p> + * + * If using the manual mode, a PacketListener should be registered that + * listens for Presence packets that have a type of + * {@link org.jivesoftware.smack.packet.Presence.Type#SUBSCRIBE}. + * + * @return the subscription mode. + */ + public int getSubscriptionMode() { + return subscriptionMode; + } + + /** + * Sets the subscription processing mode, which dictates what action + * Smack will take when subscription requests from other users are made. + * The default subscription mode is {@link #SUBSCRIPTION_ACCEPT_ALL}.<p> + * + * If using the manual mode, a PacketListener should be registered that + * listens for Presence packets that have a type of + * {@link org.jivesoftware.smack.packet.Presence.Type#SUBSCRIBE}. + * + * @param subscriptionMode the subscription mode. + */ + public void setSubscriptionMode(int subscriptionMode) { + if (subscriptionMode != SUBSCRIPTION_ACCEPT_ALL && + subscriptionMode != SUBSCRIPTION_REJECT_ALL && + subscriptionMode != SUBSCRIPTION_MANUAL) + { + throw new IllegalArgumentException("Invalid mode."); + } + this.subscriptionMode = subscriptionMode; + } + + /** + * Reloads the entire roster from the server. This is an asynchronous operation, + * which means the method will return immediately, and the roster will be + * reloaded at a later point when the server responds to the reload request. + */ + public void reload() { + connection.sendPacket(new RosterPacket()); + } + + /** + * Adds a listener to this roster. The listener will be fired anytime one or more + * changes to the roster are pushed from the server. + * + * @param rosterListener a roster listener. + */ + public void addRosterListener(RosterListener rosterListener) { + synchronized (rosterListeners) { + if (!rosterListeners.contains(rosterListener)) { + rosterListeners.add(rosterListener); + } + } + } + + /** + * Removes a listener from this roster. The listener will be fired anytime one or more + * changes to the roster are pushed from the server. + * + * @param rosterListener a roster listener. + */ + public void removeRosterListener(RosterListener rosterListener) { + synchronized (rosterListeners) { + rosterListeners.remove(rosterListener); + } + } + + /** + * Creates a new group.<p> + * + * Note: you must add at least one entry to the group for the group to be kept + * after a logout/login. This is due to the way that XMPP stores group information. + * + * @param name the name of the group. + * @return a new group. + */ + public RosterGroup createGroup(String name) { + synchronized (groups) { + if (groups.containsKey(name)) { + throw new IllegalArgumentException("Group with name " + name + " alread exists."); + } + RosterGroup group = new RosterGroup(name, connection); + groups.put(name, group); + return group; + } + } + + /** + * Creates a new roster entry and presence subscription. The server will asynchronously + * update the roster with the subscription status. + * + * @param user the user. (e.g. johndoe@jabber.org) + * @param name the nickname of the user. + * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the + * the roster entry won't belong to a group. + */ + public void createEntry(String user, String name, String [] groups) throws XMPPException { + // Create and send roster entry creation packet. + RosterPacket rosterPacket = new RosterPacket(); + rosterPacket.setType(IQ.Type.SET); + RosterPacket.Item item = new RosterPacket.Item(user, name); + if (groups != null) { + for (int i=0; i<groups.length; i++) { + if (groups[i] != null) { + item.addGroupName(groups[i]); + } + } + } + rosterPacket.addRosterItem(item); + // Wait up to a certain number of seconds for a reply from the server. + PacketCollector collector = connection.createPacketCollector( + new PacketIDFilter(rosterPacket.getPacketID())); + connection.sendPacket(rosterPacket); + IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from the server."); + } + // If the server replied with an error, throw an exception. + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + + // Create a presence subscription packet and send. + Presence presencePacket = new Presence(Presence.Type.SUBSCRIBE); + presencePacket.setTo(user); + connection.sendPacket(presencePacket); + } + + /** + * Removes a roster entry from the roster. The roster entry will also be removed from the + * unfiled entries or from any roster group where it could belong and will no longer be part + * of the roster. Note that this is an asynchronous call -- Smack must wait for the server + * to send an updated subscription status. + * + * @param entry a roster entry. + */ + public void removeEntry(RosterEntry entry) throws XMPPException { + // Only remove the entry if it's in the entry list. + // The actual removal logic takes place in RosterPacketListenerprocess>>Packet(Packet) + synchronized (entries) { + if (!entries.contains(entry)) { + return; + } + } + RosterPacket packet = new RosterPacket(); + packet.setType(IQ.Type.SET); + RosterPacket.Item item = RosterEntry.toRosterItem(entry); + // Set the item type as REMOVE so that the server will delete the entry + item.setItemType(RosterPacket.ItemType.REMOVE); + packet.addRosterItem(item); + PacketCollector collector = connection.createPacketCollector( + new PacketIDFilter(packet.getPacketID())); + connection.sendPacket(packet); + IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from the server."); + } + // If the server replied with an error, throw an exception. + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + else { + + } + } + + /** + * Returns a count of the entries in the roster. + * + * @return the number of entries in the roster. + */ + public int getEntryCount() { + HashMap entryMap = new HashMap(); + // Loop through all roster groups. + for (Iterator groups = getGroups(); groups.hasNext(); ) { + RosterGroup rosterGroup = (RosterGroup) groups.next(); + for (Iterator entries = rosterGroup.getEntries(); entries.hasNext(); ) { + entryMap.put(entries.next(), ""); + } + } + synchronized (unfiledEntries) { + return entryMap.size() + unfiledEntries.size(); + } + } + + /** + * Returns all entries in the roster, including entries that don't belong to + * any groups. + * + * @return all entries in the roster. + */ + public Iterator getEntries() { + ArrayList allEntries = new ArrayList(); + // Loop through all roster groups and add their entries to the answer + for (Iterator groups = getGroups(); groups.hasNext(); ) { + RosterGroup rosterGroup = (RosterGroup) groups.next(); + for (Iterator entries = rosterGroup.getEntries(); entries.hasNext(); ) { + RosterEntry entry = (RosterEntry)entries.next(); + if (!allEntries.contains(entry)) { + allEntries.add(entry); + } + } + } + // Add the roster unfiled entries to the answer + synchronized (unfiledEntries) { + allEntries.addAll(unfiledEntries); + } + return allEntries.iterator(); + } + + /** + * Returns a count of the unfiled entries in the roster. An unfiled entry is + * an entry that doesn't belong to any groups. + * + * @return the number of unfiled entries in the roster. + */ + public int getUnfiledEntryCount() { + synchronized (unfiledEntries) { + return unfiledEntries.size(); + } + } + + /** + * Returns an Iterator for the unfiled roster entries. An unfiled entry is + * an entry that doesn't belong to any groups. + * + * @return an iterator the unfiled roster entries. + */ + public Iterator getUnfiledEntries() { + synchronized (unfiledEntries) { + return Collections.unmodifiableList(new ArrayList(unfiledEntries)).iterator(); + } + } + + /** + * Returns the roster entry associated with the given XMPP address or + * <tt>null</tt> if the user is not an entry in the roster. + * + * @param user the XMPP address of the user (eg "jsmith@example.com"). The address could be + * in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource"). + * @return the roster entry or <tt>null</tt> if it does not exist. + */ + public RosterEntry getEntry(String user) { + if (user == null) { + return null; + } + String userLowerCase = user.toLowerCase(); + synchronized (entries) { + for (Iterator i=entries.iterator(); i.hasNext(); ) { + RosterEntry entry = (RosterEntry)i.next(); + if (entry.getUser().equals(userLowerCase)) { + return entry; + } + } + } + return null; + } + + /** + * Returns true if the specified XMPP address is an entry in the roster. + * + * @param user the XMPP address of the user (eg "jsmith@example.com"). The address could be + * in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource"). + * @return true if the XMPP address is an entry in the roster. + */ + public boolean contains(String user) { + return getEntry(user) != null; + } + + /** + * Returns the roster group with the specified name, or <tt>null</tt> if the + * group doesn't exist. + * + * @param name the name of the group. + * @return the roster group with the specified name. + */ + public RosterGroup getGroup(String name) { + synchronized (groups) { + return (RosterGroup)groups.get(name); + } + } + + /** + * Returns the number of the groups in the roster. + * + * @return the number of groups in the roster. + */ + public int getGroupCount() { + synchronized (groups) { + return groups.size(); + } + } + + /** + * Returns an iterator the for all the roster groups. + * + * @return an iterator for all roster groups. + */ + public Iterator getGroups() { + synchronized (groups) { + List groupsList = Collections.unmodifiableList(new ArrayList(groups.values())); + return groupsList.iterator(); + } + } + + /** + * Returns the presence info for a particular user, or <tt>null</tt> if the user + * is unavailable (offline) or if no presence information is available, such as + * when you are not subscribed to the user's presence updates.<p> + * + * If the user has several presences (one for each resource) then answer the presence + * with the highest priority. + * + * @param user a fully qualified xmpp ID. The address could be in any valid format (e.g. + * "domain/resource", "user@domain" or "user@domain/resource"). + * @return the user's current presence, or <tt>null</tt> if the user is unavailable + * or if no presence information is available.. + */ + public Presence getPresence(String user) { + String key = getPresenceMapKey(user); + Map userPresences = (Map) presenceMap.get(key); + if (userPresences == null) { + return null; + } + else { + // Find the resource with the highest priority + // Might be changed to use the resource with the highest availability instead. + Iterator it = userPresences.keySet().iterator(); + Presence p; + Presence presence = null; + + while (it.hasNext()) { + p = (Presence) userPresences.get(it.next()); + if (presence == null) { + presence = p; + } + else { + if (p.getPriority() > presence.getPriority()) { + presence = p; + } + } + } + return presence; + } + } + + /** + * Returns the presence info for a particular user's resource, or <tt>null</tt> if the user + * is unavailable (offline) or if no presence information is available, such as + * when you are not subscribed to the user's presence updates. + * + * @param userResource a fully qualified xmpp ID including a resource. + * @return the user's current presence, or <tt>null</tt> if the user is unavailable + * or if no presence information is available. + */ + public Presence getPresenceResource(String userResource) { + String key = getPresenceMapKey(userResource); + String resource = StringUtils.parseResource(userResource); + Map userPresences = (Map)presenceMap.get(key); + if (userPresences == null) { + return null; + } + else { + return (Presence) userPresences.get(resource); + } + } + + /** + * Returns an iterator (of Presence objects) for all the user's current presences + * or <tt>null</tt> if the user is unavailable (offline) or if no presence information + * is available, such as when you are not subscribed to the user's presence updates. + * + * @param user a fully qualified xmpp ID, e.g. jdoe@example.com + * @return an iterator (of Presence objects) for all the user's current presences, + * or <tt>null</tt> if the user is unavailable or if no presence information + * is available. + */ + public Iterator getPresences(String user) { + String key = getPresenceMapKey(user); + Map userPresences = (Map)presenceMap.get(key); + if (userPresences == null) { + return null; + } + else { + synchronized (userPresences) { + return new HashMap(userPresences).values().iterator(); + } + } + } + + /** + * Returns the key to use in the presenceMap for a fully qualified xmpp ID. The roster + * can contain any valid address format such us "domain/resource", "user@domain" or + * "user@domain/resource". If the roster contains an entry associated with the fully qualified + * xmpp ID then use the fully qualified xmpp ID as the key in presenceMap, otherwise use the + * bare address. Note: When the key in presenceMap is a fully qualified xmpp ID, the + * userPresences is useless since it will always contain one entry for the user. + * + * @param user the fully qualified xmpp ID, e.g. jdoe@example.com/Work. + * @return the key to use in the presenceMap for the fully qualified xmpp ID. + */ + private String getPresenceMapKey(String user) { + if (user == null) { + return null; + } + String key = user; + if (!contains(user)) { + key = StringUtils.parseBareAddress(user); + } + return key.toLowerCase(); + } + + /** + * Fires roster changed event to roster listeners indicating that the + * specified collections of contacts have been added, updated or deleted + * from the roster. + * + * @param addedEntries the collection of address of the added contacts. + * @param updatedEntries the collection of address of the updated contacts. + * @param deletedEntries the collection of address of the deleted contacts. + */ + private void fireRosterChangedEvent(Collection addedEntries, Collection updatedEntries, + Collection deletedEntries) { + RosterListener [] listeners = null; + synchronized (rosterListeners) { + listeners = new RosterListener[rosterListeners.size()]; + rosterListeners.toArray(listeners); + } + for (int i=0; i<listeners.length; i++) { + if (!addedEntries.isEmpty()) { + listeners[i].entriesAdded(addedEntries); + } + if (!updatedEntries.isEmpty()) { + listeners[i].entriesUpdated(updatedEntries); + } + if (!deletedEntries.isEmpty()) { + listeners[i].entriesDeleted(deletedEntries); + } + } + } + + /** + * Fires roster presence changed event to roster listeners. + */ + private void fireRosterPresenceEvent(String user) { + RosterListener [] listeners = null; + synchronized (rosterListeners) { + listeners = new RosterListener[rosterListeners.size()]; + rosterListeners.toArray(listeners); + } + for (int i=0; i<listeners.length; i++) { + listeners[i].presenceChanged(user); + } + } + + /** + * Listens for all presence packets and processes them. + */ + private class PresencePacketListener implements PacketListener { + public void processPacket(Packet packet) { + Presence presence = (Presence)packet; + String from = presence.getFrom(); + String key = getPresenceMapKey(from); + + // If an "available" packet, add it to the presence map. Each presence map will hold + // for a particular user a map with the presence packets saved for each resource. + if (presence.getType() == Presence.Type.AVAILABLE) { + Map userPresences; + // Get the user presence map + if (presenceMap.get(key) == null) { + userPresences = new HashMap(); + presenceMap.put(key, userPresences); + } + else { + userPresences = (Map)presenceMap.get(key); + } + // Add the new presence, using the resources as a key. + synchronized (userPresences) { + userPresences.put(StringUtils.parseResource(from), presence); + } + // If the user is in the roster, fire an event. + synchronized (entries) { + for (Iterator i = entries.iterator(); i.hasNext();) { + RosterEntry entry = (RosterEntry) i.next(); + if (entry.getUser().equals(key)) { + fireRosterPresenceEvent(from); + } + } + } + } + // If an "unavailable" packet, remove any entries in the presence map. + else if (presence.getType() == Presence.Type.UNAVAILABLE) { + if (presenceMap.get(key) != null) { + Map userPresences = (Map) presenceMap.get(key); + synchronized (userPresences) { + userPresences.remove(StringUtils.parseResource(from)); + } + if (userPresences.isEmpty()) { + presenceMap.remove(key); + } + } + // If the user is in the roster, fire an event. + synchronized (entries) { + for (Iterator i=entries.iterator(); i.hasNext(); ) { + RosterEntry entry = (RosterEntry)i.next(); + if (entry.getUser().equals(key)) { + fireRosterPresenceEvent(from); + } + } + } + } + else if (presence.getType() == Presence.Type.SUBSCRIBE) { + if (subscriptionMode == SUBSCRIPTION_ACCEPT_ALL) { + // Accept all subscription requests. + Presence response = new Presence(Presence.Type.SUBSCRIBED); + response.setTo(presence.getFrom()); + connection.sendPacket(response); + } + else if (subscriptionMode == SUBSCRIPTION_REJECT_ALL) { + // Reject all subscription requests. + Presence response = new Presence(Presence.Type.UNSUBSCRIBED); + response.setTo(presence.getFrom()); + connection.sendPacket(response); + } + // Otherwise, in manual mode so ignore. + } + else if (presence.getType() == Presence.Type.UNSUBSCRIBE) { + if (subscriptionMode != SUBSCRIPTION_MANUAL) { + // Acknowledge and accept unsubscription notification so that the + // server will stop sending notifications saying that the contact + // has unsubscribed to our presence. + Presence response = new Presence(Presence.Type.UNSUBSCRIBED); + response.setTo(presence.getFrom()); + connection.sendPacket(response); + } + // Otherwise, in manual mode so ignore. + } + } + } + + /** + * Listens for all roster packets and processes them. + */ + private class RosterPacketListener implements PacketListener { + + public void processPacket(Packet packet) { + // Keep a registry of the entries that were added, deleted or updated. An event + // will be fired for each affected entry + Collection addedEntries = new ArrayList(); + Collection updatedEntries = new ArrayList(); + Collection deletedEntries = new ArrayList(); + + RosterPacket rosterPacket = (RosterPacket)packet; + for (Iterator i=rosterPacket.getRosterItems(); i.hasNext(); ) { + RosterPacket.Item item = (RosterPacket.Item)i.next(); + RosterEntry entry = new RosterEntry(item.getUser(), item.getName(), + item.getItemType(), item.getItemStatus(), connection); + + // If the packet is of the type REMOVE then remove the entry + if (RosterPacket.ItemType.REMOVE.equals(item.getItemType())) { + // Remove the entry from the entry list. + if (entries.contains(entry)) { + entries.remove(entry); + } + // Remove the entry from the unfiled entry list. + synchronized (unfiledEntries) { + if (unfiledEntries.contains(entry)) { + unfiledEntries.remove(entry); + } + } + // Removing the user from the roster, so remove any presence information + // about them. + String key = StringUtils.parseName(item.getUser()) + "@" + + StringUtils.parseServer(item.getUser()); + presenceMap.remove(key); + // Keep note that an entry has been removed + deletedEntries.add(item.getUser()); + } + else { + // Make sure the entry is in the entry list. + if (!entries.contains(entry)) { + entries.add(entry); + // Keep note that an entry has been added + addedEntries.add(item.getUser()); + } + else { + // If the entry was in then list then update its state with the new values + RosterEntry existingEntry = + (RosterEntry) entries.get(entries.indexOf(entry)); + existingEntry + .updateState(entry.getName(), entry.getType(), entry.getStatus()); + // Keep note that an entry has been updated + updatedEntries.add(item.getUser()); + } + // If the roster entry belongs to any groups, remove it from the + // list of unfiled entries. + if (item.getGroupNames().hasNext()) { + synchronized (unfiledEntries) { + unfiledEntries.remove(entry); + } + } + // Otherwise add it to the list of unfiled entries. + else { + synchronized (unfiledEntries) { + if (!unfiledEntries.contains(entry)) { + unfiledEntries.add(entry); + } + } + } + } + + // Find the list of groups that the user currently belongs to. + List currentGroupNames = new ArrayList(); + for (Iterator j = entry.getGroups(); j.hasNext(); ) { + RosterGroup group = (RosterGroup)j.next(); + currentGroupNames.add(group.getName()); + } + + // If the packet is not of the type REMOVE then add the entry to the groups + if (!RosterPacket.ItemType.REMOVE.equals(item.getItemType())) { + // Create the new list of groups the user belongs to. + List newGroupNames = new ArrayList(); + for (Iterator k = item.getGroupNames(); k.hasNext(); ) { + String groupName = (String)k.next(); + // Add the group name to the list. + newGroupNames.add(groupName); + + // Add the entry to the group. + RosterGroup group = getGroup(groupName); + if (group == null) { + group = createGroup(groupName); + groups.put(groupName, group); + } + // Add the entry. + group.addEntryLocal(entry); + } + + // We have the list of old and new group names. We now need to + // remove the entry from the all the groups it may no longer belong + // to. We do this by subracting the new group set from the old. + for (int m=0; m<newGroupNames.size(); m++) { + currentGroupNames.remove(newGroupNames.get(m)); + } + } + + // Loop through any groups that remain and remove the entries. + // This is neccessary for the case of remote entry removals. + for (int n=0; n<currentGroupNames.size(); n++) { + String groupName = (String)currentGroupNames.get(n); + RosterGroup group = getGroup(groupName); + group.removeEntryLocal(entry); + if (group.getEntryCount() == 0) { + synchronized (groups) { + groups.remove(groupName); + } + } + } + // Remove all the groups with no entries. We have to do this because + // RosterGroup.removeEntry removes the entry immediately (locally) and the + // group could remain empty. + // TODO Check the performance/logic for rosters with large number of groups + for (Iterator it = getGroups(); it.hasNext();) { + RosterGroup group = (RosterGroup)it.next(); + if (group.getEntryCount() == 0) { + synchronized (groups) { + groups.remove(group.getName()); + } + } + } + } + + // Mark the roster as initialized. + synchronized (Roster.this) { + rosterInitialized = true; + Roster.this.notifyAll(); + } + + // Fire event for roster listeners. + fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries); + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterEntry.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterEntry.java new file mode 100644 index 000000000..d972c2bf6 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterEntry.java @@ -0,0 +1,193 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.packet.RosterPacket; +import org.jivesoftware.smack.packet.IQ; + +import java.util.*; + +/** + * Each user in your roster is represented by a roster entry, which contains the user's + * JID and a name or nickname you assign. + * + * @author Matt Tucker + */ +public class RosterEntry { + + private String user; + private String name; + private RosterPacket.ItemType type; + private RosterPacket.ItemStatus status; + private XMPPConnection connection; + + /** + * Creates a new roster entry. + * + * @param user the user. + * @param name the nickname for the entry. + * @param type the subscription type. + * @param status the subscription status (related to subscriptions pending to be approbed). + * @param connection a connection to the XMPP server. + */ + RosterEntry(String user, String name, RosterPacket.ItemType type, + RosterPacket.ItemStatus status, XMPPConnection connection) { + this.user = user; + this.name = name; + this.type = type; + this.status = status; + this.connection = connection; + } + + /** + * Returns the JID of the user associated with this entry. + * + * @return the user associated with this entry. + */ + public String getUser() { + return user; + } + + /** + * Returns the name associated with this entry. + * + * @return the name. + */ + public String getName() { + return name; + } + + /** + * Sets the name associated with this entry. + * + * @param name the name. + */ + public void setName(String name) { + // Do nothing if the name hasn't changed. + if (name != null && name.equals(this.name)) { + return; + } + this.name = name; + RosterPacket packet = new RosterPacket(); + packet.setType(IQ.Type.SET); + packet.addRosterItem(toRosterItem(this)); + connection.sendPacket(packet); + } + + /** + * Updates the state of the entry with the new values. + * + * @param name the nickname for the entry. + * @param type the subscription type. + * @param status the subscription status (related to subscriptions pending to be approbed). + */ + void updateState(String name, RosterPacket.ItemType type, RosterPacket.ItemStatus status) { + this.name = name; + this.type = type; + this.status = status; + } + + /** + * Returns an iterator for all the roster groups that this entry belongs to. + * + * @return an iterator for the groups this entry belongs to. + */ + public Iterator getGroups() { + List results = new ArrayList(); + // Loop through all roster groups and find the ones that contain this + // entry. This algorithm should be fine + for (Iterator i=connection.roster.getGroups(); i.hasNext(); ) { + RosterGroup group = (RosterGroup)i.next(); + if (group.contains(this)) { + results.add(group); + } + } + return results.iterator(); + } + + /** + * Returns the roster subscription type of the entry. When the type is + * {@link RosterPacket.ItemType#NONE} or {@link RosterPacket.ItemType#FROM}, + * refer to {@link RosterEntry getStatus()} to see if a subscription request + * is pending. + * + * @return the type. + */ + public RosterPacket.ItemType getType() { + return type; + } + + /** + * Returns the roster subscription status of the entry. When the status is + * RosterPacket.ItemStatus.SUBSCRIPTION_PENDING, the contact has to answer the + * subscription request. + * + * @return the status. + */ + public RosterPacket.ItemStatus getStatus() { + return status; + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + if (name != null) { + buf.append(name).append(": "); + } + buf.append(user); + Iterator groups = getGroups(); + if (groups.hasNext()) { + buf.append(" ["); + RosterGroup group = (RosterGroup)groups.next(); + buf.append(group.getName()); + while (groups.hasNext()) { + buf.append(", "); + group = (RosterGroup)groups.next(); + buf.append(group.getName()); + } + buf.append("]"); + } + return buf.toString(); + } + + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object != null && object instanceof RosterEntry) { + return user.equals(((RosterEntry)object).getUser()); + } + else { + return false; + } + } + + static RosterPacket.Item toRosterItem(RosterEntry entry) { + RosterPacket.Item item = new RosterPacket.Item(entry.getUser(), entry.getName()); + item.setItemType(entry.getType()); + item.setItemStatus(entry.getStatus()); + // Set the correct group names for the item. + for (Iterator j=entry.getGroups(); j.hasNext(); ) { + RosterGroup group = (RosterGroup)j.next(); + item.addGroupName(group.getName()); + } + return item; + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterGroup.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterGroup.java new file mode 100644 index 000000000..c84dd15c9 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterGroup.java @@ -0,0 +1,252 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.packet.RosterPacket; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.filter.PacketIDFilter; + +import java.util.*; + +/** + * A group of roster entries. + * + * @see Roster#getGroup(String) + * @author Matt Tucker + */ +public class RosterGroup { + + private String name; + private XMPPConnection connection; + private List entries; + + /** + * Creates a new roster group instance. + * + * @param name the name of the group. + * @param connection the connection the group belongs to. + */ + RosterGroup(String name, XMPPConnection connection) { + this.name = name; + this.connection = connection; + entries = new ArrayList(); + } + + /** + * Returns the name of the group. + * + * @return the name of the group. + */ + public String getName() { + return name; + } + + /** + * Sets the name of the group. Changing the group's name is like moving all the group entries + * of the group to a new group specified by the new name. Since this group won't have entries + * it will be removed from the roster. This means that all the references to this object will + * be invalid and will need to be updated to the new group specified by the new name. + * + * @param name the name of the group. + */ + public void setName(String name) { + synchronized (entries) { + for (int i=0; i<entries.size(); i++) { + RosterPacket packet = new RosterPacket(); + packet.setType(IQ.Type.SET); + RosterEntry entry = (RosterEntry)entries.get(i); + RosterPacket.Item item = RosterEntry.toRosterItem(entry); + item.removeGroupName(this.name); + item.addGroupName(name); + packet.addRosterItem(item); + connection.sendPacket(packet); + } + } + } + + /** + * Returns the number of entries in the group. + * + * @return the number of entries in the group. + */ + public int getEntryCount() { + synchronized (entries) { + return entries.size(); + } + } + + /** + * Returns an iterator for the entries in the group. + * + * @return an iterator for the entries in the group. + */ + public Iterator getEntries() { + synchronized (entries) { + return Collections.unmodifiableList(new ArrayList(entries)).iterator(); + } + } + + /** + * Returns the roster entry associated with the given XMPP address or + * <tt>null</tt> if the user is not an entry in the group. + * + * @param user the XMPP address of the user (eg "jsmith@example.com"). + * @return the roster entry or <tt>null</tt> if it does not exist in the group. + */ + public RosterEntry getEntry(String user) { + if (user == null) { + return null; + } + // Roster entries never include a resource so remove the resource + // if it's a part of the XMPP address. + user = StringUtils.parseBareAddress(user); + String userLowerCase = user.toLowerCase(); + synchronized (entries) { + for (Iterator i=entries.iterator(); i.hasNext(); ) { + RosterEntry entry = (RosterEntry)i.next(); + if (entry.getUser().equals(userLowerCase)) { + return entry; + } + } + } + return null; + } + + /** + * Returns true if the specified entry is part of this group. + * + * @param entry a roster entry. + * @return true if the entry is part of this group. + */ + public boolean contains(RosterEntry entry) { + synchronized (entries) { + return entries.contains(entry); + } + } + + /** + * Returns true if the specified XMPP address is an entry in this group. + * + * @param user the XMPP address of the user. + * @return true if the XMPP address is an entry in this group. + */ + public boolean contains(String user) { + return getEntry(user) != null; + } + + /** + * Adds a roster entry to this group. If the entry was unfiled then it will be removed from + * the unfiled list and will be added to this group. + * + * @param entry a roster entry. + * @throws XMPPException if an error occured while trying to add the entry to the group. + */ + public void addEntry(RosterEntry entry) throws XMPPException { + PacketCollector collector = null; + // Only add the entry if it isn't already in the list. + synchronized (entries) { + if (!entries.contains(entry)) { + RosterPacket packet = new RosterPacket(); + packet.setType(IQ.Type.SET); + RosterPacket.Item item = RosterEntry.toRosterItem(entry); + item.addGroupName(getName()); + packet.addRosterItem(item); + // Wait up to a certain number of seconds for a reply from the server. + collector = connection + .createPacketCollector(new PacketIDFilter(packet.getPacketID())); + connection.sendPacket(packet); + } + } + if (collector != null) { + IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from the server."); + } + // If the server replied with an error, throw an exception. + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + // Add the new entry to the group since the server processed the request successfully + addEntryLocal(entry); + } + } + + /** + * Removes a roster entry from this group. If the entry does not belong to any other group + * then it will be considered as unfiled, therefore it will be added to the list of unfiled + * entries. + * + * @param entry a roster entry. + * @throws XMPPException if an error occured while trying to remove the entry from the group. + */ + public void removeEntry(RosterEntry entry) throws XMPPException { + PacketCollector collector = null; + // Only remove the entry if it's in the entry list. + // Remove the entry locally, if we wait for RosterPacketListenerprocess>>Packet(Packet) + // to take place the entry will exist in the group until a packet is received from the + // server. + synchronized (entries) { + if (entries.contains(entry)) { + RosterPacket packet = new RosterPacket(); + packet.setType(IQ.Type.SET); + RosterPacket.Item item = RosterEntry.toRosterItem(entry); + item.removeGroupName(this.getName()); + packet.addRosterItem(item); + // Wait up to a certain number of seconds for a reply from the server. + collector = connection + .createPacketCollector(new PacketIDFilter(packet.getPacketID())); + connection.sendPacket(packet); + } + } + if (collector != null) { + IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from the server."); + } + // If the server replied with an error, throw an exception. + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + // Remove the entry locally since the server processed the request successfully + removeEntryLocal(entry); + } + } + + void addEntryLocal(RosterEntry entry) { + // Only add the entry if it isn't already in the list. + synchronized (entries) { + entries.remove(entry); + entries.add(entry); + } + } + + void removeEntryLocal(RosterEntry entry) { + // Only remove the entry if it's in the entry list. + synchronized (entries) { + if (entries.contains(entry)) { + entries.remove(entry); + } + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterListener.java new file mode 100644 index 000000000..fbc6ce8e7 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/RosterListener.java @@ -0,0 +1,62 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import java.util.Collection; + +/** + * A listener that is fired any time a roster is changed or the presence of + * a user in the roster is changed. + * + * @author Matt Tucker + */ +public interface RosterListener { + + /** + * Called when roster entries are added. + * + * @param addresses the XMPP addresses of the contacts that have been added to the roster. + */ + public void entriesAdded(Collection addresses); + + /** + * Called when a roster entries are updated. + * + * @param addresses the XMPP addresses of the contacts whose entries have been updated. + */ + public void entriesUpdated(Collection addresses); + + /** + * Called when a roster entries are removed. + * + * @param addresses the XMPP addresses of the contacts that have been removed from the roster. + */ + public void entriesDeleted(Collection addresses); + + /** + * Called when the presence of a roster entry is changed. + * + * @param XMPPAddress the XMPP address of the user who's presence has changed, + * including the resource. + */ + public void presenceChanged(String XMPPAddress); +} + diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SASLAuthentication.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SASLAuthentication.java new file mode 100644 index 000000000..03d8c0928 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SASLAuthentication.java @@ -0,0 +1,417 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.Bind; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Session; +import org.jivesoftware.smack.sasl.SASLAnonymous; +import org.jivesoftware.smack.sasl.SASLMechanism; +import org.jivesoftware.smack.sasl.SASLPlainMechanism; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.*; + +/** + * This class is responsible authenticating the user using SASL, binding the resource + * to the connection and establishing a session with the server.<p> + * + * Once TLS has been negotiated (i.e. the connection has been secured) it is possible to + * register with the server, authenticate using Non-SASL or authenticate using SASL. If the + * server supports SASL then Smack will first try to authenticate using SASL. But if that + * fails then Non-SASL will be tried.<p> + * + * The server may support many SASL mechanisms to use for authenticating. Out of the box + * Smack provides SASL PLAIN but it is possible to register new SASL Mechanisms. Use + * {@link #registerSASLMechanism(int, String, Class)} to add new mechanisms. See + * {@link SASLMechanism}.<p> + * + * Once the user has been authenticated with SASL, it is necessary to bind a resource for + * the connection. If no resource is passed in {@link #authenticate(String, String, String)} + * then the server will assign a resource for the connection. In case a resource is passed + * then the server will receive the desired resource but may assign a modified resource for + * the connection.<p> + * + * Once a resource has been binded and if the server supports sessions then Smack will establish + * a session so that instant messaging and presence functionalities may be used. + * + * @author Gaston Dombiak + */ +public class SASLAuthentication implements UserAuthentication { + + private static Map implementedMechanisms = new HashMap(); + private static List mechanismsPreferences = new ArrayList(); + + private XMPPConnection connection; + private Collection serverMechanisms = new ArrayList(); + private SASLMechanism currentMechanism = null; + /** + * Boolean indicating if SASL negotiation has finished and was successful. + */ + private boolean saslNegotiated = false; + /** + * Boolean indication if SASL authentication has failed. When failed the server may end + * the connection. + */ + private boolean saslFailed = false; + private boolean resourceBinded = false; + private boolean sessionSupported = false; + + static { + // Register SASL mechanisms supported by Smack + registerSASLMechanism(0, "PLAIN", SASLPlainMechanism.class); + } + + /** + * Registers a new SASL mechanism in the specified preference position. The client will try + * to authenticate using the most prefered SASL mechanism that is also supported by the server. + * <p/> + * <p/> + * Use the <tt>index</tt> parameter to set the level of preference of the new SASL mechanism. + * A value of 0 means that the mechanism is the most prefered one. + * + * @param index preference position amongst all the implemented SASL mechanism. Starts with 0. + * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. + * @param mClass a SASLMechanism subclass. + */ + public static void registerSASLMechanism(int index, String name, Class mClass) { + implementedMechanisms.put(name, mClass); + mechanismsPreferences.add(index, name); + } + + /** + * Unregisters an existing SASL mechanism. Once the mechanism has been unregistered it won't + * be possible to authenticate users using the removed SASL mechanism. + * + * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. + */ + public static void unregisterSASLMechanism(String name) { + implementedMechanisms.remove(name); + mechanismsPreferences.remove(name); + } + + /** + * Returns the registerd SASLMechanism classes sorted by the level of preference. + * + * @return the registerd SASLMechanism classes sorted by the level of preference. + */ + public static List getRegisterSASLMechanisms() { + List answer = new ArrayList(); + for (Iterator it = mechanismsPreferences.iterator(); it.hasNext();) { + answer.add(implementedMechanisms.get(it.next())); + } + return answer; + } + + SASLAuthentication(XMPPConnection connection) { + super(); + this.connection = connection; + } + + /** + * Returns true if the server offered ANONYMOUS SASL as a way to authenticate users. + * + * @return true if the server offered ANONYMOUS SASL as a way to authenticate users. + */ + public boolean hasAnonymousAuthentication() { + return serverMechanisms.contains("ANONYMOUS"); + } + + /** + * Returns true if the server offered SASL authentication besides ANONYMOUS SASL. + * + * @return true if the server offered SASL authentication besides ANONYMOUS SASL. + */ + public boolean hasNonAnonymousAuthentication() { + if (!serverMechanisms.isEmpty()) { + // Check that anonymous sasl is not the only supported mechanism + if (serverMechanisms.size() == 1) { + return !hasAnonymousAuthentication(); + } + return true; + } + return false; + } + + /** + * Performs SASL authentication of the specified user. If SASL authentication was successful + * then resource binding and session establishment will be performed. This method will return + * the full JID provided by the server while binding a resource to the connection.<p> + * + * The server may assign a full JID with a username or resource different than the requested + * by this method. + * + * @param username the username that is authenticating with the server. + * @param password the password to send to the server. + * @param resource the desired resource. + * @return the full JID provided by the server while binding a resource to the connection. + * @throws XMPPException if an error occures while authenticating. + */ + public String authenticate(String username, String password, String resource) + throws XMPPException { + // Locate the SASLMechanism to use + Class selected = null; + for (Iterator it = mechanismsPreferences.iterator(); it.hasNext();) { + String mechanism = (String) it.next(); + if (implementedMechanisms.containsKey(mechanism) && + serverMechanisms.contains(mechanism)) { + selected = (Class) implementedMechanisms.get(mechanism); + break; + } + } + if (selected != null) { + // A SASL mechanism was found. Authenticate using the selected mechanism and then + // proceed to bind a resource + try { + Constructor constructor = selected + .getConstructor(new Class[]{SASLAuthentication.class}); + currentMechanism = (SASLMechanism) constructor.newInstance(new Object[]{this}); + // Trigger SASL authentication with the selected mechanism + currentMechanism.authenticate(username, connection.getServiceName(), password); + + // Wait until SASL negotiation finishes + synchronized (this) { + if (!saslNegotiated && !saslFailed) { + try { + wait(30000); + } catch (InterruptedException e) { + } + } + } + + if (saslFailed) { + // SASL authentication failed and the server may have closed the connection + // so throw an exception + throw new XMPPException("SASL authentication failed"); + } + + if (saslNegotiated) { + // Bind a resource for this connection and + return bindResourceAndEstablishSession(resource); + } else { + // SASL authentication failed so try a Non-SASL authentication + return new NonSASLAuthentication(connection) + .authenticate(username, password, resource); + } + } + catch (XMPPException e) { + throw e; + } + catch (Exception e) { + e.printStackTrace(); + // SASL authentication failed so try a Non-SASL authentication + return new NonSASLAuthentication(connection) + .authenticate(username, password, resource); + } + } else { + // No SASL method was found so try a Non-SASL authentication + return new NonSASLAuthentication(connection).authenticate(username, password, resource); + } + } + + /** + * Performs ANONYMOUS SASL authentication. If SASL authentication was successful + * then resource binding and session establishment will be performed. This method will return + * the full JID provided by the server while binding a resource to the connection.<p> + * + * The server will assign a full JID with a randomly generated resource and possibly with + * no username. + * + * @return the full JID provided by the server while binding a resource to the connection. + * @throws XMPPException if an error occures while authenticating. + */ + public String authenticateAnonymously() throws XMPPException { + try { + currentMechanism = new SASLAnonymous(this); + currentMechanism.authenticate(null, null, null); + + // Wait until SASL negotiation finishes + synchronized (this) { + if (!saslNegotiated && !saslFailed) { + try { + wait(5000); + } catch (InterruptedException e) { + } + } + } + + if (saslFailed) { + // SASL authentication failed and the server may have closed the connection + // so throw an exception + throw new XMPPException("SASL authentication failed"); + } + + if (saslNegotiated) { + // Bind a resource for this connection and + return bindResourceAndEstablishSession(null); + } + else { + return new NonSASLAuthentication(connection).authenticateAnonymously(); + } + } catch (IOException e) { + return new NonSASLAuthentication(connection).authenticateAnonymously(); + } + } + + private String bindResourceAndEstablishSession(String resource) throws XMPPException { + // Wait until server sends response containing the <bind> element + synchronized (this) { + if (!resourceBinded) { + try { + wait(30000); + } catch (InterruptedException e) { + } + } + } + + if (!resourceBinded) { + // Server never offered resource binding + throw new XMPPException("Resource binding not offered by server"); + } + + Bind bindResource = new Bind(); + bindResource.setResource(resource); + + PacketCollector collector = connection + .createPacketCollector(new PacketIDFilter(bindResource.getPacketID())); + // Send the packet + connection.sendPacket(bindResource); + // Wait up to a certain number of seconds for a response from the server. + Bind response = (Bind) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from the server."); + } + // If the server replied with an error, throw an exception. + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + String userJID = response.getJid(); + + if (sessionSupported) { + Session session = new Session(); + collector = connection.createPacketCollector(new PacketIDFilter(session.getPacketID())); + // Send the packet + connection.sendPacket(session); + // Wait up to a certain number of seconds for a response from the server. + IQ ack = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (ack == null) { + throw new XMPPException("No response from the server."); + } + // If the server replied with an error, throw an exception. + else if (ack.getType() == IQ.Type.ERROR) { + throw new XMPPException(ack.getError()); + } + } + else { + // Server never offered session establishment + throw new XMPPException("Session establishment not offered by server"); + } + return userJID; + } + + /** + * Sets the available SASL mechanism reported by the server. The server will report the + * available SASL mechanism once the TLS negotiation was successful. This information is + * stored and will be used when doing the authentication for logging in the user. + * + * @param mechanisms collection of strings with the available SASL mechanism reported + * by the server. + */ + void setAvailableSASLMethods(Collection mechanisms) { + this.serverMechanisms = mechanisms; + } + + /** + * Returns true if the user was able to authenticate with the server usins SASL. + * + * @return true if the user was able to authenticate with the server usins SASL. + */ + public boolean isAuthenticated() { + return saslNegotiated; + } + + /** + * The server is challenging the SASL authentication we just sent. Forward the challenge + * to the current SASLMechanism we are using. The SASLMechanism will send a response to + * the server. The length of the challenge-response sequence varies according to the + * SASLMechanism in use. + * + * @param challenge a base64 encoded string representing the challenge. + * @throws IOException If a network error occures while authenticating. + */ + void challengeReceived(String challenge) throws IOException { + currentMechanism.challengeReceived(challenge); + } + + /** + * Notification message saying that SASL authentication was successful. The next step + * would be to bind the resource. + */ + void authenticated() { + synchronized (this) { + saslNegotiated = true; + // Wake up the thread that is waiting in the #authenticate method + notify(); + } + } + + /** + * Notification message saying that SASL authentication has failed. The server may have + * closed the connection depending on the number of possible retries. + */ + void authenticationFailed() { + synchronized (this) { + saslFailed = true; + // Wake up the thread that is waiting in the #authenticate method + notify(); + } + } + + /** + * Notification message saying that the server requires the client to bind a + * resource to the stream. + */ + void bindingRequired() { + synchronized (this) { + resourceBinded = true; + // Wake up the thread that is waiting in the #authenticate method + notify(); + } + } + + public void send(String stanza) throws IOException { + connection.writer.write(stanza); + connection.writer.flush(); + } + + /** + * Notification message saying that the server supports sessions. When a server supports + * sessions the client needs to send a Session packet after successfully binding a resource + * for the session. + */ + void sessionsSupported() { + sessionSupported = true; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SSLXMPPConnection.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SSLXMPPConnection.java new file mode 100644 index 000000000..bb3a72f98 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SSLXMPPConnection.java @@ -0,0 +1,160 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +/** + * Creates an SSL connection to a XMPP server using the legacy dedicated SSL port + * mechanism. Fully compliant XMPP 1.0 servers (e.g. Wildfire 2.4.0) do not + * require using a dedicated SSL port. Instead, TLS (a standardized version of SSL 3.0) + * is dynamically negotiated over the standard XMPP port. Therefore, only use this + * class to connect to an XMPP server if you know that the server does not support + * XMPP 1.0 TLS connections. + * + * @author Matt Tucker + */ +public class SSLXMPPConnection extends XMPPConnection { + + private static SocketFactory socketFactory = new DummySSLSocketFactory(); + + /** + * Creates a new SSL connection to the specified host on the default + * SSL port (5223). The IP address of the server is assumed to match the + * service name. + * + * @param host the XMPP host. + * @throws XMPPException if an error occurs while trying to establish the connection. + * Two possible errors can occur which will be wrapped by an XMPPException -- + * UnknownHostException (XMPP error code 504), and IOException (XMPP error code + * 502). The error codes and wrapped exceptions can be used to present more + * appropiate error messages to end-users. + */ + public SSLXMPPConnection(String host) throws XMPPException { + this(host, 5223); + } + + /** + * Creates a new SSL connection to the specified host on the specified port. The IP address + * of the server is assumed to match the service name. + * + * @param host the XMPP host. + * @param port the port to use for the connection (default XMPP SSL port is 5223). + * @throws XMPPException if an error occurs while trying to establish the connection. + * Two possible errors can occur which will be wrapped by an XMPPException -- + * UnknownHostException (XMPP error code 504), and IOException (XMPP error code + * 502). The error codes and wrapped exceptions can be used to present more + * appropiate error messages to end-users. + */ + public SSLXMPPConnection(String host, int port) throws XMPPException { + this(host, port, host); + } + + /** + * Creates a new SSL connection to the specified XMPP server on the given host and port. + * + * @param host the host name, or null for the loopback address. + * @param port the port on the server that should be used (default XMPP SSL port is 5223). + * @param serviceName the name of the XMPP server to connect to; e.g. <tt>jivesoftware.com</tt>. + * @throws XMPPException if an error occurs while trying to establish the connection. + * Two possible errors can occur which will be wrapped by an XMPPException -- + * UnknownHostException (XMPP error code 504), and IOException (XMPP error code + * 502). The error codes and wrapped exceptions can be used to present more + * appropiate error messages to end-users. + */ + public SSLXMPPConnection(String host, int port, String serviceName) throws XMPPException { + super(host, port, serviceName, socketFactory); + } + + public boolean isSecureConnection() { + return true; + } + + /** + * An SSL socket factory that will let any certifacte past, even if it's expired or + * not singed by a root CA. + */ + private static class DummySSLSocketFactory extends SSLSocketFactory { + + private SSLSocketFactory factory; + + public DummySSLSocketFactory() { + + try { + SSLContext sslcontent = SSLContext.getInstance("TLS"); + sslcontent.init(null, // KeyManager not required + new TrustManager[] { new OpenTrustManager() }, + new java.security.SecureRandom()); + factory = sslcontent.getSocketFactory(); + } + catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + catch (KeyManagementException e) { + e.printStackTrace(); + } + } + + public static SocketFactory getDefault() { + return new DummySSLSocketFactory(); + } + + public Socket createSocket(Socket socket, String s, int i, boolean flag) + throws IOException + { + return factory.createSocket(socket, s, i, flag); + } + + public Socket createSocket(InetAddress inaddr, int i, InetAddress inaddr2, int j) + throws IOException + { + return factory.createSocket(inaddr, i, inaddr2, j); + } + + public Socket createSocket(InetAddress inaddr, int i) throws IOException { + return factory.createSocket(inaddr, i); + } + + public Socket createSocket(String s, int i, InetAddress inaddr, int j) throws IOException { + return factory.createSocket(s, i, inaddr, j); + } + + public Socket createSocket(String s, int i) throws IOException { + return factory.createSocket(s, i); + } + + public String[] getDefaultCipherSuites() { + return factory.getSupportedCipherSuites(); + } + + public String[] getSupportedCipherSuites() { + return factory.getSupportedCipherSuites(); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ServerTrustManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ServerTrustManager.java new file mode 100644 index 000000000..9e5fff001 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/ServerTrustManager.java @@ -0,0 +1,184 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import javax.net.ssl.X509TrustManager; +import java.io.FileInputStream; +import java.security.*; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; + +/** + * Trust manager that checks all certificates presented by the server. This class + * is used during TLS negotiation. It is possible to disable/enable some or all checkings + * by configuring the {@link ConnectionConfiguration}. The truststore file that contains + * knows and trusted CA root certificates can also be configure in {@link ConnectionConfiguration}. + * + * @author Gaston Dombiak + */ +class ServerTrustManager implements X509TrustManager { + + private ConnectionConfiguration configuration; + + /** + * Holds the domain of the remote server we are trying to connect + */ + private String server; + private KeyStore trustStore; + + public ServerTrustManager(String server, ConnectionConfiguration configuration) { + this.configuration = configuration; + this.server = server; + + try { + trustStore = KeyStore.getInstance(configuration.getTruststoreType()); + trustStore.load(new FileInputStream(configuration.getTruststorePath()), + configuration.getTruststorePassword().toCharArray()); + } + catch (Exception e) { + e.printStackTrace(); + // Disable root CA checking + configuration.setVerifyRootCAEnabled(false); + } + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(X509Certificate[] arg0, String arg1) + throws CertificateException { + } + + public void checkServerTrusted(X509Certificate[] x509Certificates, String arg1) + throws CertificateException { + + int nSize = x509Certificates.length; + + String peerIdentity = getPeerIdentity(x509Certificates[0]); + + if (configuration.isVerifyChainEnabled()) { + // Working down the chain, for every certificate in the chain, + // verify that the subject of the certificate is the issuer of the + // next certificate in the chain. + Principal principalLast = null; + for (int i = nSize -1; i >= 0 ; i--) { + X509Certificate x509certificate = x509Certificates[i]; + Principal principalIssuer = x509certificate.getIssuerDN(); + Principal principalSubject = x509certificate.getSubjectDN(); + if (principalLast != null) { + if (principalIssuer.equals(principalLast)) { + try { + PublicKey publickey = + x509Certificates[i + 1].getPublicKey(); + x509Certificates[i].verify(publickey); + } + catch (GeneralSecurityException generalsecurityexception) { + throw new CertificateException( + "signature verification failed of " + peerIdentity); + } + } + else { + throw new CertificateException( + "subject/issuer verification failed of " + peerIdentity); + } + } + principalLast = principalSubject; + } + } + + if (configuration.isVerifyRootCAEnabled()) { + // Verify that the the last certificate in the chain was issued + // by a third-party that the client trusts. + boolean trusted = false; + try { + trusted = trustStore.getCertificateAlias(x509Certificates[nSize - 1]) != null; + if (!trusted && nSize == 1 && configuration.isSelfSignedCertificateEnabled()) + { + System.out.println("Accepting self-signed certificate of remote server: " + + peerIdentity); + trusted = true; + } + } + catch (KeyStoreException e) { + e.printStackTrace(); + } + if (!trusted) { + throw new CertificateException("root certificate not trusted of " + peerIdentity); + } + } + + if (configuration.isNotMatchingDomainCheckEnabled()) { + // Verify that the first certificate in the chain corresponds to + // the server we desire to authenticate. + // Check if the certificate uses a wildcard indicating that subdomains are valid + if (peerIdentity.startsWith("*.")) { + // Remove the wildcard + peerIdentity = peerIdentity.substring(2); + // Check if the requested subdomain matches the certified domain + if (!server.endsWith(peerIdentity)) { + throw new CertificateException("target verification failed of " + peerIdentity); + } + } + else if (!server.equals(peerIdentity)) { + throw new CertificateException("target verification failed of " + peerIdentity); + } + } + + if (configuration.isExpiredCertificatesCheckEnabled()) { + // For every certificate in the chain, verify that the certificate + // is valid at the current time. + Date date = new Date(); + for (int i = 0; i < nSize; i++) { + try { + x509Certificates[i].checkValidity(date); + } + catch (GeneralSecurityException generalsecurityexception) { + throw new CertificateException("invalid date of " + server); + } + } + } + + } + + /** + * Returns the identity of the remote server as defined in the specified certificate. The + * identity is defined in the subjectDN of the certificate and it can also be defined in + * the subjectAltName extension of type "xmpp". When the extension is being used then the + * identity defined in the extension in going to be returned. Otherwise, the value stored in + * the subjectDN is returned. + * + * @param x509Certificate the certificate the holds the identity of the remote server. + * @return the identity of the remote server as defined in the specified certificate. + */ + public static String getPeerIdentity(X509Certificate x509Certificate) { + Principal principalSubject = x509Certificate.getSubjectDN(); + // TODO Look the identity in the subjectAltName extension if available + String name = principalSubject.getName(); + if (name.startsWith("CN=")) { + // Remove the CN= prefix + name = name.substring(3); + } + return name; + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SmackConfiguration.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SmackConfiguration.java new file mode 100644 index 000000000..250f372d6 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/SmackConfiguration.java @@ -0,0 +1,207 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.xmlpull.mxp1.MXParser; +import org.xmlpull.v1.XmlPullParser; + +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; + +/** + * Represents the configuration of Smack. The configuration is used for: + * <ul> + * <li> Initializing classes by loading them at start-up. + * <li> Getting the current Smack version. + * <li> Getting and setting global library behavior, such as the period of time + * to wait for replies to packets from the server. Note: setting these values + * via the API will override settings in the configuration file. + * </ul> + * + * Configuration settings are stored in META-INF/smack-config.xml (typically inside the + * smack.jar file). + * + * @author Gaston Dombiak + */ +public final class SmackConfiguration { + + private static final String SMACK_VERSION = "2.2.0"; + + private static int packetReplyTimeout = 5000; + private static int keepAliveInterval = 30000; + + private SmackConfiguration() { + } + + /** + * Loads the configuration from the smack-config.xml file.<p> + * + * So far this means that: + * 1) a set of classes will be loaded in order to execute their static init block + * 2) retrieve and set the current Smack release + */ + static { + try { + // Get an array of class loaders to try loading the providers files from. + ClassLoader[] classLoaders = getClassLoaders(); + for (int i = 0; i < classLoaders.length; i++) { + Enumeration configEnum = classLoaders[i].getResources("META-INF/smack-config.xml"); + while (configEnum.hasMoreElements()) { + URL url = (URL) configEnum.nextElement(); + InputStream systemStream = null; + try { + systemStream = url.openStream(); + XmlPullParser parser = new MXParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(systemStream, "UTF-8"); + int eventType = parser.getEventType(); + do { + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("className")) { + // Attempt to load the class so that the class can get initialized + parseClassToLoad(parser); + } + else if (parser.getName().equals("packetReplyTimeout")) { + packetReplyTimeout = parseIntProperty(parser, packetReplyTimeout); + } + else if (parser.getName().equals("keepAliveInterval")) { + keepAliveInterval = parseIntProperty(parser, keepAliveInterval); + } + } + eventType = parser.next(); + } + while (eventType != XmlPullParser.END_DOCUMENT); + } + catch (Exception e) { + e.printStackTrace(); + } + finally { + try { + systemStream.close(); + } + catch (Exception e) { + } + } + } + } + } + catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Returns the Smack version information, eg "1.3.0". + * + * @return the Smack version information. + */ + public static String getVersion() { + return SMACK_VERSION; + } + + /** + * Returns the number of milliseconds to wait for a response from + * the server. The default value is 5000 ms. + * + * @return the milliseconds to wait for a response from the server + */ + public static int getPacketReplyTimeout() { + // The timeout value must be greater than 0 otherwise we will answer the default value + if (packetReplyTimeout <= 0) { + packetReplyTimeout = 5000; + } + return packetReplyTimeout; + } + + /** + * Sets the number of milliseconds to wait for a response from + * the server. + * + * @param timeout the milliseconds to wait for a response from the server + */ + public static void setPacketReplyTimeout(int timeout) { + if (timeout <= 0) { + throw new IllegalArgumentException(); + } + packetReplyTimeout = timeout; + } + + /** + * Returns the number of milleseconds delay between sending keep-alive + * requests to the server. The default value is 30000 ms. A value of -1 + * mean no keep-alive requests will be sent to the server. + * + * @return the milliseconds to wait between keep-alive requests, or -1 if + * no keep-alive should be sent. + */ + public static int getKeepAliveInterval() { + return keepAliveInterval; + } + + /** + * Sets the number of milleseconds delay between sending keep-alive + * requests to the server. The default value is 30000 ms. A value of -1 + * mean no keep-alive requests will be sent to the server. + * + * @param interval the milliseconds to wait between keep-alive requests, + * or -1 if no keep-alive should be sent. + */ + public static void setKeepAliveInterval(int interval) { + keepAliveInterval = interval; + } + + private static void parseClassToLoad(XmlPullParser parser) throws Exception { + String className = parser.nextText(); + // Attempt to load the class so that the class can get initialized + try { + Class.forName(className); + } + catch (ClassNotFoundException cnfe) { + System.err.println("Error! A startup class specified in smack-config.xml could " + + "not be loaded: " + className); + } + } + + private static int parseIntProperty(XmlPullParser parser, int defaultValue) + throws Exception + { + try { + return Integer.parseInt(parser.nextText()); + } + catch (NumberFormatException nfe) { + nfe.printStackTrace(); + return defaultValue; + } + } + + /** + * Returns an array of class loaders to load resources from. + * + * @return an array of ClassLoader instances. + */ + private static ClassLoader[] getClassLoaders() { + ClassLoader[] classLoaders = new ClassLoader[2]; + classLoaders[0] = new SmackConfiguration().getClass().getClassLoader(); + classLoaders[1] = Thread.currentThread().getContextClassLoader(); + return classLoaders; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/UserAuthentication.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/UserAuthentication.java new file mode 100644 index 000000000..2d3a319ac --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/UserAuthentication.java @@ -0,0 +1,55 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +/** + * There are two ways to authenticate a user with a server. Using SASL or Non-SASL + * authentication. This interface makes {@link SASLAuthentication} and + * {@link NonSASLAuthentication} polyphormic. + * + * @author Gaston Dombiak + */ +interface UserAuthentication { + + /** + * Authenticates the user with the server. This method will return the full JID provided by + * the server. The server may assign a full JID with a username and resource different than + * the requested by this method. + * + * @param username the username that is authenticating with the server. + * @param password the password to send to the server. + * @param resource the desired resource. + * @return the full JID provided by the server while binding a resource for the connection. + * @throws XMPPException if an error occures while authenticating. + */ + String authenticate(String username, String password, String resource) throws + XMPPException; + + /** + * Performs an anonymous authentication with the server. The server will created a new full JID + * for this connection. An exception will be thrown if the server does not support anonymous + * authentication. + * + * @return the full JID provided by the server while binding a resource for the connection. + * @throws XMPPException if an error occures while authenticating. + */ + String authenticateAnonymously() throws XMPPException; +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPConnection.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPConnection.java new file mode 100644 index 000000000..ce5be5c5f --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPConnection.java @@ -0,0 +1,1249 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.debugger.SmackDebugger; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.DNSUtil; +import org.jivesoftware.smack.util.StringUtils; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import java.io.*; +import java.lang.ref.WeakReference; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.*; + +/** + * Creates a connection to a XMPP server. A simple use of this API might + * look like the following: + * <pre> + * // Create a connection to the jivesoftware.com XMPP server. + * XMPPConnection con = new XMPPConnection("jivesoftware.com"); + * // Most servers require you to login before performing other tasks. + * con.login("jsmith", "mypass"); + * // Start a new conversation with John Doe and send him a message. + * Chat chat = con.createChat("jdoe@jabber.org"); + * chat.sendMessage("Hey, how's it going?"); + * </pre> + * + * @author Matt Tucker + */ +public class XMPPConnection { + + /** + * Value that indicates whether debugging is enabled. When enabled, a debug + * window will apear for each new connection that will contain the following + * information:<ul> + * <li> Client Traffic -- raw XML traffic generated by Smack and sent to the server. + * <li> Server Traffic -- raw XML traffic sent by the server to the client. + * <li> Interpreted Packets -- shows XML packets from the server as parsed by Smack. + * </ul> + * + * Debugging can be enabled by setting this field to true, or by setting the Java system + * property <tt>smack.debugEnabled</tt> to true. The system property can be set on the + * command line such as "java SomeApp -Dsmack.debugEnabled=true". + */ + public static boolean DEBUG_ENABLED = false; + + private static List connectionEstablishedListeners = new ArrayList(); + + static { + // Use try block since we may not have permission to get a system + // property (for example, when an applet). + try { + DEBUG_ENABLED = Boolean.getBoolean("smack.debugEnabled"); + } + catch (Exception e) { + // Ignore. + } + // Ensure the SmackConfiguration class is loaded by calling a method in it. + SmackConfiguration.getVersion(); + } + private SmackDebugger debugger = null; + + /** + * IP address or host name of the server. This information is only used when + * creating new socket connections to the server. If this information is not + * configured then it will be assumed that the host name matches the service name. + */ + String host; + int port; + Socket socket; + + /** + * Hostname of the XMPP server. Usually servers use the same service name as the name + * of the server. However, there are some servers like google where host would be + * talk.google.com and the serviceName would be gmail.com. + */ + String serviceName; + + String connectionID; + private String user = null; + private boolean connected = false; + private boolean authenticated = false; + private boolean anonymous = false; + private boolean usingTLS = false; + + PacketWriter packetWriter; + PacketReader packetReader; + + Roster roster = null; + private AccountManager accountManager = null; + private SASLAuthentication saslAuthentication = new SASLAuthentication(this); + + Writer writer; + Reader reader; + + /** + * A map between JIDs and the most recently created Chat object with that JID. + * Reference to the Chat is stored via a WeakReference so that the map + * does not interfere with garbage collection. The map of chats must be stored + * with each connection. + */ + Map chats = Collections.synchronizedMap(new HashMap()); + + /** + * Collection of available stream compression methods offered by the server. + */ + private Collection compressionMethods; + /** + * Flag that indicates if stream compression is actually in use. + */ + private boolean usingCompression; + /** + * Holds the initial configuration used while creating the connection. + */ + private ConnectionConfiguration configuration; + + /** + * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be + * performed to try to determine the IP address and port corresponding to the + * serviceName; if that lookup fails, it's assumed that server resides at serviceName + * with the default port of 5222. This is the preferred constructor for connecting + * to an XMPP server. + * + * @param serviceName the name of the XMPP server to connect to; e.g. <tt>jivesoftware.com</tt>. + * @throws XMPPException if an error occurs while trying to establish the connection. + * Two possible errors can occur which will be wrapped by an XMPPException -- + * UnknownHostException (XMPP error code 504), and IOException (XMPP error code + * 502). The error codes and wrapped exceptions can be used to present more + * appropiate error messages to end-users. + */ + public XMPPConnection(String serviceName) throws XMPPException { + // Perform DNS lookup to get host and port to use + DNSUtil.HostAddress address = DNSUtil.resolveXMPPDomain(serviceName); + // Create the configuration for this new connection + ConnectionConfiguration config = + new ConnectionConfiguration(address.getHost(), address.getPort(), serviceName); + config.setTLSEnabled(true); + config.setCompressionEnabled(false); + config.setSASLAuthenticationEnabled(true); + config.setDebuggerEnabled(DEBUG_ENABLED); + // Set the new connection configuration + connectUsingConfiguration(config, null); + } + + /** + * Creates a new connection to the XMPP server at the specifiec host and port. + * + * @param host the name of the XMPP server to connect to; e.g. <tt>jivesoftware.com</tt>. + * @param port the port on the server that should be used; e.g. <tt>5222</tt>. + * @throws XMPPException if an error occurs while trying to establish the connection. + * Two possible errors can occur which will be wrapped by an XMPPException -- + * UnknownHostException (XMPP error code 504), and IOException (XMPP error code + * 502). The error codes and wrapped exceptions can be used to present more + * appropiate error messages to end-users. + */ + public XMPPConnection(String host, int port) throws XMPPException { + // Create the configuration for this new connection + ConnectionConfiguration config = new ConnectionConfiguration(host, port); + config.setTLSEnabled(true); + config.setCompressionEnabled(false); + config.setSASLAuthenticationEnabled(true); + config.setDebuggerEnabled(DEBUG_ENABLED); + // Set the new connection configuration + connectUsingConfiguration(config, null); + } + + /** + * Creates a new connection to the specified XMPP server on the given host and port. + * + * @param host the host name, or null for the loopback address. + * @param port the port on the server that should be used; e.g. <tt>5222</tt>. + * @param serviceName the name of the XMPP server to connect to; e.g. <tt>jivesoftware.com</tt>. + * @throws XMPPException if an error occurs while trying to establish the connection. + * Two possible errors can occur which will be wrapped by an XMPPException -- + * UnknownHostException (XMPP error code 504), and IOException (XMPP error code + * 502). The error codes and wrapped exceptions can be used to present more + * appropiate error messages to end-users. + */ + public XMPPConnection(String host, int port, String serviceName) throws XMPPException { + // Create the configuration for this new connection + ConnectionConfiguration config = new ConnectionConfiguration(host, port, serviceName); + config.setTLSEnabled(true); + config.setCompressionEnabled(false); + config.setSASLAuthenticationEnabled(true); + config.setDebuggerEnabled(DEBUG_ENABLED); + // Set the new connection configuration + connectUsingConfiguration(config, null); + } + + /** + * Creates a new connection to the specified XMPP server on the given port using the + * specified SocketFactory.<p> + * + * A custom SocketFactory allows fine-grained control of the actual connection to the + * XMPP server. A typical use for a custom SocketFactory is when connecting through a + * SOCKS proxy. + * + * @param host the host name, or null for the loopback address. + * @param port the port on the server that should be used; e.g. <tt>5222</tt>. + * @param serviceName the name of the XMPP server to connect to; e.g. <tt>jivesoftware.com</tt>. + * @param socketFactory a SocketFactory that will be used to create the socket to the XMPP + * server. + * @throws XMPPException if an error occurs while trying to establish the connection. + * Two possible errors can occur which will be wrapped by an XMPPException -- + * UnknownHostException (XMPP error code 504), and IOException (XMPP error code + * 502). The error codes and wrapped exceptions can be used to present more + * appropiate error messages to end-users. + */ + public XMPPConnection(String host, int port, String serviceName, SocketFactory socketFactory) + throws XMPPException + { + // Create the configuration for this new connection + ConnectionConfiguration config = new ConnectionConfiguration(host, port, serviceName); + config.setTLSEnabled(true); + config.setCompressionEnabled(false); + config.setSASLAuthenticationEnabled(true); + config.setDebuggerEnabled(DEBUG_ENABLED); + // Set the new connection configuration + connectUsingConfiguration(config, socketFactory); + } + + public XMPPConnection(ConnectionConfiguration config) throws XMPPException { + // Set the new connection configuration + connectUsingConfiguration(config, null); + } + + public XMPPConnection(ConnectionConfiguration config, SocketFactory socketFactory) + throws XMPPException { + // Set the new connection configuration + connectUsingConfiguration(config, socketFactory); + } + + private void connectUsingConfiguration(ConnectionConfiguration config, + SocketFactory socketFactory) throws XMPPException { + this.host = config.getHost(); + this.port = config.getPort(); + try { + if (socketFactory == null) { + this.socket = new Socket(host, port); + } + else { + this.socket = socketFactory.createSocket(host, port); + } + } + catch (UnknownHostException uhe) { + throw new XMPPException( + "Could not connect to " + host + ":" + port + ".", + new XMPPError(504), + uhe); + } + catch (IOException ioe) { + throw new XMPPException( + "XMPPError connecting to " + host + ":" + port + ".", + new XMPPError(502), + ioe); + } + this.serviceName = config.getServiceName(); + try { + // Keep a copy to be sure that once the configuration has been passed to the + // constructor it cannot be modified + this.configuration = (ConnectionConfiguration) config.clone(); + } + catch (CloneNotSupportedException e) {} + init(); + } + + /** + * Package-private default constructor. This constructor is only intended + * for unit testing. Normal classes extending XMPPConnection should override + * one of the other constructors. + */ + XMPPConnection() { + } + + /** + * Returns the connection ID for this connection, which is the value set by the server + * when opening a XMPP stream. If the server does not set a connection ID, this value + * will be null. + * + * @return the ID of this connection returned from the XMPP server. + */ + public String getConnectionID() { + return connectionID; + } + + /** + * Returns the name of the service provided by the XMPP server for this connection. After + * authenticating with the server the returned value may be different. + * + * @return the name of the service provided by the XMPP server. + */ + public String getServiceName() { + return serviceName; + } + + /** + * Returns the host name of the server where the XMPP server is running. This would be the + * IP address of the server or a name that may be resolved by a DNS server. + * + * @return the host name of the server where the XMPP server is running. + */ + public String getHost() { + return host; + } + + /** + * Returns the port number of the XMPP server for this connection. The default port + * for normal connections is 5222. The default port for SSL connections is 5223. + * + * @return the port number of the XMPP server. + */ + public int getPort() { + return port; + } + + /** + * Returns the full XMPP address of the user that is logged in to the connection or + * <tt>null</tt> if not logged in yet. An XMPP address is in the form + * username@server/resource. + * + * @return the full XMPP address of the user logged in. + */ + public String getUser() { + if (!isAuthenticated()) { + return null; + } + return user; + } + + /** + * Logs in to the server using the strongest authentication mode supported by + * the server, then set our presence to available. If more than five seconds + * (default timeout) elapses in each step of the authentication process without + * a response from the server, or if an error occurs, a XMPPException will be thrown. + * + * @param username the username. + * @param password the password. + * @throws XMPPException if an error occurs. + */ + public void login(String username, String password) throws XMPPException { + login(username, password, "Smack"); + } + + /** + * Logs in to the server using the strongest authentication mode supported by + * the server, then sets presence to available. If more than five seconds + * (default timeout) elapses in each step of the authentication process without + * a response from the server, or if an error occurs, a XMPPException will be thrown. + * + * @param username the username. + * @param password the password. + * @param resource the resource. + * @throws XMPPException if an error occurs. + * @throws IllegalStateException if not connected to the server, or already logged in + * to the serrver. + */ + public synchronized void login(String username, String password, String resource) + throws XMPPException + { + login(username, password, resource, true); + } + + /** + * Logs in to the server using the strongest authentication mode supported by + * the server. If the server supports SASL authentication then the user will be + * authenticated using SASL if not Non-SASL authentication will be tried. An available + * presence may optionally be sent. If <tt>sendPresence</tt> + * is false, a presence packet must be sent manually later. If more than five seconds + * (default timeout) elapses in each step of the authentication process without a + * response from the server, or if an error occurs, a XMPPException will be thrown. + * + * @param username the username. + * @param password the password. + * @param resource the resource. + * @param sendPresence if <tt>true</tt> an available presence will be sent automatically + * after login is completed. + * @throws XMPPException if an error occurs. + * @throws IllegalStateException if not connected to the server, or already logged in + * to the serrver. + */ + public synchronized void login(String username, String password, String resource, + boolean sendPresence) throws XMPPException + { + if (!isConnected()) { + throw new IllegalStateException("Not connected to server."); + } + if (authenticated) { + throw new IllegalStateException("Already logged in to server."); + } + // Do partial version of nameprep on the username. + username = username.toLowerCase().trim(); + + String response = null; + if (configuration.isSASLAuthenticationEnabled() && + saslAuthentication.hasNonAnonymousAuthentication()) { + // Authenticate using SASL + response = saslAuthentication.authenticate(username, password, resource); + } + else { + // Authenticate using Non-SASL + response = new NonSASLAuthentication(this).authenticate(username, password, resource); + } + + // Set the user. + if (response != null) { + this.user = response; + // Update the serviceName with the one returned by the server + this.serviceName = StringUtils.parseServer(response); + } + else { + this.user = username + "@" + this.serviceName; + if (resource != null) { + this.user += "/" + resource; + } + } + + // If compression is enabled then request the server to use stream compression + if (configuration.isCompressionEnabled()) { + useCompression(); + } + + // Create the roster. + this.roster = new Roster(this); + roster.reload(); + + // Set presence to online. + if (sendPresence) { + packetWriter.sendPacket(new Presence(Presence.Type.AVAILABLE)); + } + + // Indicate that we're now authenticated. + authenticated = true; + + packetReader.notifyConnectionAuthenticated(); + + anonymous = false; + + // If debugging is enabled, change the the debug window title to include the + // name we are now logged-in as. + // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger + // will be null + if (configuration.isDebuggerEnabled() && debugger != null) { + debugger.userHasLogged(user); + } + } + + /** + * Logs in to the server anonymously. Very few servers are configured to support anonymous + * authentication, so it's fairly likely logging in anonymously will fail. If anonymous login + * does succeed, your XMPP address will likely be in the form "server/123ABC" (where "123ABC" + * is a random value generated by the server). + * + * @throws XMPPException if an error occurs or anonymous logins are not supported by the server. + * @throws IllegalStateException if not connected to the server, or already logged in + * to the serrver. + */ + public synchronized void loginAnonymously() throws XMPPException { + if (!isConnected()) { + throw new IllegalStateException("Not connected to server."); + } + if (authenticated) { + throw new IllegalStateException("Already logged in to server."); + } + + String response = null; + if (configuration.isSASLAuthenticationEnabled() && + saslAuthentication.hasAnonymousAuthentication()) { + response = saslAuthentication.authenticateAnonymously(); + } + else { + // Authenticate using Non-SASL + response = new NonSASLAuthentication(this).authenticateAnonymously(); + } + + // Set the user value. + this.user = response; + // Update the serviceName with the one returned by the server + this.serviceName = StringUtils.parseServer(response); + + // If compression is enabled then request the server to use stream compression + if (configuration.isCompressionEnabled()) { + useCompression(); + } + + // Anonymous users can't have a roster. + roster = null; + + // Set presence to online. + packetWriter.sendPacket(new Presence(Presence.Type.AVAILABLE)); + + // Indicate that we're now authenticated. + authenticated = true; + + packetReader.notifyConnectionAuthenticated(); + + anonymous = true; + + // If debugging is enabled, change the the debug window title to include the + // name we are now logged-in as. + // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger + // will be null + if (configuration.isDebuggerEnabled() && debugger != null) { + debugger.userHasLogged(user); + } + } + + /** + * Returns the roster for the user logged into the server. If the user has not yet + * logged into the server (or if the user is logged in anonymously), this method will return + * <tt>null</tt>. + * + * @return the user's roster, or <tt>null</tt> if the user has not logged in yet. + */ + public Roster getRoster() { + if (roster == null) { + return null; + } + // If this is the first time the user has asked for the roster after calling + // login, we want to wait for the server to send back the user's roster. This + // behavior shields API users from having to worry about the fact that roster + // operations are asynchronous, although they'll still have to listen for + // changes to the roster. Note: because of this waiting logic, internal + // Smack code should be wary about calling the getRoster method, and may need to + // access the roster object directly. + if (!roster.rosterInitialized) { + try { + synchronized (roster) { + long waitTime = SmackConfiguration.getPacketReplyTimeout(); + long start = System.currentTimeMillis(); + while (!roster.rosterInitialized) { + if (waitTime <= 0) { + break; + } + roster.wait(waitTime); + long now = System.currentTimeMillis(); + waitTime -= now - start; + start = now; + } + } + } + catch (InterruptedException ie) { + // Ignore. + } + } + return roster; + } + + /** + * Returns an account manager instance for this connection. + * + * @return an account manager for this connection. + */ + public synchronized AccountManager getAccountManager() { + if (accountManager == null) { + accountManager = new AccountManager(this); + } + return accountManager; + } + + /** + * Creates a new chat with the specified participant. The participant should + * be a valid XMPP user such as <tt>jdoe@jivesoftware.com</tt> or + * <tt>jdoe@jivesoftware.com/work</tt>. + * + * @param participant the person to start the conversation with. + * @return a new Chat object. + */ + public Chat createChat(String participant) { + if (!isConnected()) { + throw new IllegalStateException("Not connected to server."); + } + return new Chat(this, participant); + } + + /** + * Creates a new group chat connected to the specified room. The room name + * should be full address, such as <tt>room@chat.example.com</tt>. + * <p> + * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com + * for the XMPP server example.com). You must ensure that the room address you're + * trying to connect to includes the proper chat sub-domain. + * + * @param room the fully qualifed name of the room. + * @return a new GroupChat object. + */ + public GroupChat createGroupChat(String room) { + if (!isConnected()) { + throw new IllegalStateException("Not connected to server."); + } + return new GroupChat(this, room); + } + + /** + * Returns true if currently connected to the XMPP server. + * + * @return true if connected. + */ + public boolean isConnected() { + return connected; + } + + /** + * Returns true if the connection is a secured one, such as an SSL connection or + * if TLS was negotiated successfully. + * + * @return true if a secure connection to the server. + */ + public boolean isSecureConnection() { + return isUsingTLS(); + } + + /** + * Returns true if currently authenticated by successfully calling the login method. + * + * @return true if authenticated. + */ + public boolean isAuthenticated() { + return authenticated; + } + + /** + * Returns true if currently authenticated anonymously. + * + * @return true if authenticated anonymously. + */ + public boolean isAnonymous() { + return anonymous; + } + + /** + * Closes the connection by setting presence to unavailable then closing the stream to + * the XMPP server. Once a connection has been closed, it cannot be re-opened. + */ + public void close() { + // Set presence to offline. + packetWriter.sendPacket(new Presence(Presence.Type.UNAVAILABLE)); + packetReader.shutdown(); + packetWriter.shutdown(); + // Wait 150 ms for processes to clean-up, then shutdown. + try { + Thread.sleep(150); + } + catch (Exception e) { + // Ignore. + } + + // Close down the readers and writers. + if (reader != null) + { + try { reader.close(); } catch (Throwable ignore) { /* ignore */ } + reader = null; + } + if (writer != null) + { + try { writer.close(); } catch (Throwable ignore) { /* ignore */ } + writer = null; + } + + try { + socket.close(); + } + catch (Exception e) { + // Ignore. + } + authenticated = false; + connected = false; + } + + /** + * Sends the specified packet to the server. + * + * @param packet the packet to send. + */ + public void sendPacket(Packet packet) { + if (!isConnected()) { + throw new IllegalStateException("Not connected to server."); + } + if (packet == null) { + throw new NullPointerException("Packet is null."); + } + packetWriter.sendPacket(packet); + } + + /** + * Registers a packet listener with this connection. A packet filter determines + * which packets will be delivered to the listener. + * + * @param packetListener the packet listener to notify of new packets. + * @param packetFilter the packet filter to use. + */ + public void addPacketListener(PacketListener packetListener, PacketFilter packetFilter) { + if (!isConnected()) { + throw new IllegalStateException("Not connected to server."); + } + packetReader.addPacketListener(packetListener, packetFilter); + } + + /** + * Removes a packet listener from this connection. + * + * @param packetListener the packet listener to remove. + */ + public void removePacketListener(PacketListener packetListener) { + packetReader.removePacketListener(packetListener); + } + + /** + * Registers a packet listener with this connection. The listener will be + * notified of every packet that this connection sends. A packet filter determines + * which packets will be delivered to the listener. Note that the thread + * that writes packets will be used to invoke the listeners. Therefore, each + * packet listener should complete all operations quickly or use a different + * thread for processing. + * + * @param packetListener the packet listener to notify of sent packets. + * @param packetFilter the packet filter to use. + */ + public void addPacketWriterListener(PacketListener packetListener, PacketFilter packetFilter) { + if (!isConnected()) { + throw new IllegalStateException("Not connected to server."); + } + packetWriter.addPacketListener(packetListener, packetFilter); + } + + /** + * Removes a packet listener from this connection. + * + * @param packetListener the packet listener to remove. + */ + public void removePacketWriterListener(PacketListener packetListener) { + packetWriter.removePacketListener(packetListener); + } + + /** + * Registers a packet interceptor with this connection. The interceptor will be + * invoked every time a packet is about to be sent by this connection. Interceptors + * may modify the packet to be sent. A packet filter determines which packets + * will be delivered to the interceptor. + * + * @param packetInterceptor the packet interceptor to notify of packets about to be sent. + * @param packetFilter the packet filter to use. + */ + public void addPacketWriterInterceptor(PacketInterceptor packetInterceptor, + PacketFilter packetFilter) { + if (!isConnected()) { + throw new IllegalStateException("Not connected to server."); + } + packetWriter.addPacketInterceptor(packetInterceptor, packetFilter); + } + + /** + * Removes a packet interceptor. + * + * @param packetInterceptor the packet interceptor to remove. + */ + public void removePacketWriterInterceptor(PacketInterceptor packetInterceptor) { + packetWriter.removePacketInterceptor(packetInterceptor); + } + + /** + * Creates a new packet collector for this connection. A packet filter determines + * which packets will be accumulated by the collector. + * + * @param packetFilter the packet filter to use. + * @return a new packet collector. + */ + public PacketCollector createPacketCollector(PacketFilter packetFilter) { + return packetReader.createPacketCollector(packetFilter); + } + + /** + * Adds a connection listener to this connection that will be notified when + * the connection closes or fails. + * + * @param connectionListener a connection listener. + */ + public void addConnectionListener(ConnectionListener connectionListener) { + if (connectionListener == null) { + return; + } + synchronized (packetReader.connectionListeners) { + if (!packetReader.connectionListeners.contains(connectionListener)) { + packetReader.connectionListeners.add(connectionListener); + } + } + } + + /** + * Removes a connection listener from this connection. + * + * @param connectionListener a connection listener. + */ + public void removeConnectionListener(ConnectionListener connectionListener) { + synchronized (packetReader.connectionListeners) { + packetReader.connectionListeners.remove(connectionListener); + } + } + + /** + * Adds a connection established listener that will be notified when a new connection + * is established. + * + * @param connectionEstablishedListener a listener interested on connection established events. + */ + public static void addConnectionListener(ConnectionEstablishedListener connectionEstablishedListener) { + synchronized (connectionEstablishedListeners) { + if (!connectionEstablishedListeners.contains(connectionEstablishedListener)) { + connectionEstablishedListeners.add(connectionEstablishedListener); + } + } + } + + /** + * Removes a listener on new established connections. + * + * @param connectionEstablishedListener a listener interested on connection established events. + */ + public static void removeConnectionListener(ConnectionEstablishedListener connectionEstablishedListener) { + synchronized (connectionEstablishedListeners) { + connectionEstablishedListeners.remove(connectionEstablishedListener); + } + } + + /** + * Initializes the connection by creating a packet reader and writer and opening a + * XMPP stream to the server. + * + * @throws XMPPException if establishing a connection to the server fails. + */ + private void init() throws XMPPException { + // Set the reader and writer instance variables + initReaderAndWriter(); + + try + { + packetWriter = new PacketWriter(this); + packetReader = new PacketReader(this); + + // If debugging is enabled, we should start the thread that will listen for + // all packets and then log them. + if (configuration.isDebuggerEnabled()) { + packetReader.addPacketListener(debugger.getReaderListener(), null); + if (debugger.getWriterListener() != null) { + packetWriter.addPacketListener(debugger.getWriterListener(), null); + } + } + // Start the packet writer. This will open a XMPP stream to the server + packetWriter.startup(); + // Start the packet reader. The startup() method will block until we + // get an opening stream packet back from server. + packetReader.startup(); + + // Make note of the fact that we're now connected. + connected = true; + + // Notify that a new connection has been established + connectionEstablished(this); + + // Add a listener for all message packets so that we can deliver errant + // messages to the best Chat instance available. + addPacketListener(new PacketListener() { + public void processPacket(Packet packet) { + Message message = (Message)packet; + // Ignore any messages with a thread ID, as they will likely + // already be associated with a Chat. This will miss messages + // with new thread ID values, but we can only assume that a + // listener is registered to deal with this case. + if (message.getThread() == null && + message.getType() != Message.Type.GROUP_CHAT && + message.getType() != Message.Type.HEADLINE) { + WeakReference chatRef = (WeakReference)chats.get( + StringUtils.parseBareAddress(message.getFrom())); + if (chatRef != null) { + // Do some extra clean-up if the reference was cleared. + Chat chat; + if ((chat = (Chat)chatRef.get()) == null) { + chats.remove(message.getFrom()); + } + else { + chat.deliver(message); + } + } + } + } + }, new PacketTypeFilter(Message.class)); + } + catch (XMPPException ex) { + // An exception occurred in setting up the connection. Make sure we shut down the + // readers and writers and close the socket. + + if (packetWriter != null) { + try { packetWriter.shutdown(); } catch (Throwable ignore) { /* ignore */ } + packetWriter = null; + } + if (packetReader != null) { + try { packetReader.shutdown(); } catch (Throwable ignore) { /* ignore */ } + packetReader = null; + } + if (reader != null) { + try { reader.close(); } catch (Throwable ignore) { /* ignore */ } + reader = null; + } + if (writer != null) { + try { writer.close(); } catch (Throwable ignore) { /* ignore */} + writer = null; + } + if (socket != null) { + try { socket.close(); } catch (Exception e) { /* ignore */ } + socket = null; + } + authenticated = false; + connected = false; + + throw ex; // Everything stoppped. Now throw the exception. + } + } + + private void initReaderAndWriter() throws XMPPException { + try { + if (!usingCompression) { + reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); + writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); + } + else { + try { + Class zoClass = Class.forName("com.jcraft.jzlib.ZOutputStream"); + //ZOutputStream out = new ZOutputStream(socket.getOutputStream(), JZlib.Z_BEST_COMPRESSION); + Constructor constructor = + zoClass.getConstructor(new Class[]{OutputStream.class, Integer.TYPE}); + Object out = constructor.newInstance(new Object[] {socket.getOutputStream(), new Integer(9)}); + //out.setFlushMode(JZlib.Z_PARTIAL_FLUSH); + Method method = zoClass.getMethod("setFlushMode", new Class[] {Integer.TYPE}); + method.invoke(out, new Object[] {new Integer(1)}); + writer = new BufferedWriter(new OutputStreamWriter((OutputStream) out, "UTF-8")); + + Class ziClass = Class.forName("com.jcraft.jzlib.ZInputStream"); + //ZInputStream in = new ZInputStream(socket.getInputStream()); + constructor = ziClass.getConstructor(new Class[]{InputStream.class}); + Object in = constructor.newInstance(new Object[] {socket.getInputStream()}); + //in.setFlushMode(JZlib.Z_PARTIAL_FLUSH); + method = ziClass.getMethod("setFlushMode", new Class[] {Integer.TYPE}); + method.invoke(in, new Object[] {new Integer(1)}); + reader = new BufferedReader(new InputStreamReader((InputStream) in, "UTF-8")); + } + catch (Exception e) { + e.printStackTrace(); + reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); + writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); + } + } + } + catch (IOException ioe) { + throw new XMPPException( + "XMPPError establishing connection with server.", + new XMPPError(502), + ioe); + } + + // If debugging is enabled, we open a window and write out all network traffic. + if (configuration.isDebuggerEnabled()) { + if (debugger == null) { + // Detect the debugger class to use. + String className = null; + // Use try block since we may not have permission to get a system + // property (for example, when an applet). + try { + className = System.getProperty("smack.debuggerClass"); + } + catch (Throwable t) { + } + Class debuggerClass = null; + if (className != null) { + try { + debuggerClass = Class.forName(className); + } + catch (Exception e) { + e.printStackTrace(); + } + } + if (debuggerClass == null) { + try { + debuggerClass = + Class.forName("org.jivesoftware.smackx.debugger.EnhancedDebugger"); + } + catch (Exception ex) { + try { + debuggerClass = Class.forName("org.jivesoftware.smack.debugger.LiteDebugger"); + } + catch (Exception ex2) { + ex2.printStackTrace(); + } + } + } + // Create a new debugger instance. If an exception occurs then disable the debugging + // option + try { + Constructor constructor = + debuggerClass.getConstructor( + new Class[] { XMPPConnection.class, Writer.class, Reader.class }); + debugger = (SmackDebugger) constructor + .newInstance(new Object[]{this, writer, reader}); + reader = debugger.getReader(); + writer = debugger.getWriter(); + } + catch (Exception e) { + e.printStackTrace(); + DEBUG_ENABLED = false; + } + } + else { + // Obtain new reader and writer from the existing debugger + reader = debugger.newConnectionReader(reader); + writer = debugger.newConnectionWriter(writer); + } + } + } + + /** + * Fires listeners on connection established events. + */ + private static void connectionEstablished(XMPPConnection connection) { + ConnectionEstablishedListener[] listeners = null; + synchronized (connectionEstablishedListeners) { + listeners = new ConnectionEstablishedListener[connectionEstablishedListeners.size()]; + connectionEstablishedListeners.toArray(listeners); + } + for (int i = 0; i < listeners.length; i++) { + listeners[i].connectionEstablished(connection); + } + } + + /*********************************************** + * TLS code below + **********************************************/ + + /** + * Returns true if the connection to the server has successfully negotiated TLS. Once TLS + * has been negotiatied the connection has been secured. + * + * @return true if the connection to the server has successfully negotiated TLS. + */ + public boolean isUsingTLS() { + return usingTLS; + } + + /** + * Returns the SASLAuthentication manager that is responsible for authenticating with + * the server. + * + * @return the SASLAuthentication manager that is responsible for authenticating with + * the server. + */ + public SASLAuthentication getSASLAuthentication() { + return saslAuthentication; + } + + /** + * Notification message saying that the server supports TLS so confirm the server that we + * want to secure the connection. + */ + void startTLSReceived() { + if (!configuration.isTLSEnabled()) { + // Do not secure the connection using TLS since TLS was disabled + return; + } + try { + writer.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>"); + writer.flush(); + } + catch (IOException e) { + packetReader.notifyConnectionError(e); + } + } + + /** + * The server has indicated that TLS negotiation can start. We now need to secure the + * existing plain connection and perform a handshake. This method won't return until the + * connection has finished the handshake or an error occured while securing the connection. + */ + void proceedTLSReceived() throws Exception { + SSLContext context = SSLContext.getInstance("TLS"); + // Verify certificate presented by the server + context.init(null, // KeyManager not required + new javax.net.ssl.TrustManager[]{new ServerTrustManager(serviceName, configuration)}, + new java.security.SecureRandom()); + Socket plain = socket; + // Secure the plain connection + socket = context.getSocketFactory().createSocket(plain, + plain.getInetAddress().getHostName(), plain.getPort(), true); + socket.setSoTimeout(0); + socket.setKeepAlive(true); + // Initialize the reader and writer with the new secured version + initReaderAndWriter(); + // Proceed to do the handshake + ((SSLSocket) socket).startHandshake(); + + // Set that TLS was successful + usingTLS = true; + + // Set the new writer to use + packetWriter.setWriter(writer); + // Send a new opening stream to the server + packetWriter.openStream(); + } + + /** + * Sets the available stream compression methods offered by the server. + * + * @param methods compression methods offered by the server. + */ + void setAvailableCompressionMethods(Collection methods) { + compressionMethods = methods; + } + + /** + * Returns true if the specified compression method was offered by the server. + * + * @param method the method to check. + * @return true if the specified compression method was offered by the server. + */ + private boolean hasAvailableCompressionMethod(String method) { + return compressionMethods != null && compressionMethods.contains(method); + } + + /** + * Returns true if network traffic is being compressed. When using stream compression network + * traffic can be reduced up to 90%. Therefore, stream compression is ideal when using a slow + * speed network connection. However, the server will need to use more CPU time in order to + * un/compress network data so under high load the server performance might be affected.<p> + * + * Note: To use stream compression the smackx.jar file has to be present in the classpath. + * + * @return true if network traffic is being compressed. + */ + public boolean isUsingCompression() { + return usingCompression; + } + + /** + * Starts using stream compression that will compress network traffic. Traffic can be + * reduced up to 90%. Therefore, stream compression is ideal when using a slow speed network + * connection. However, the server and the client will need to use more CPU time in order to + * un/compress network data so under high load the server performance might be affected.<p> + * + * Stream compression has to have been previously offered by the server. Currently only the + * zlib method is supported by the client. Stream compression negotiation has to be done + * before authentication took place.<p> + * + * Note: To use stream compression the smackx.jar file has to be present in the classpath. + * + * @return true if stream compression negotiation was successful. + */ + private boolean useCompression() { + // If stream compression was offered by the server and we want to use + // compression then send compression request to the server + if (authenticated) { + throw new IllegalStateException("Compression should be negotiated before authentication."); + } + try { + Class.forName("com.jcraft.jzlib.ZOutputStream"); + } + catch (ClassNotFoundException e) { + throw new IllegalStateException("Cannot use compression. Add smackx.jar to the classpath"); + } + if (hasAvailableCompressionMethod("zlib")) { + requestStreamCompression(); + // Wait until compression is being used or a timeout happened + synchronized (this) { + try { + this.wait(SmackConfiguration.getPacketReplyTimeout() * 5); + } + catch (InterruptedException e) {} + } + return usingCompression; + } + return false; + } + + /** + * Request the server that we want to start using stream compression. When using TLS + * then negotiation of stream compression can only happen after TLS was negotiated. If TLS + * compression is being used the stream compression should not be used. + */ + private void requestStreamCompression() { + try { + writer.write("<compress xmlns='http://jabber.org/protocol/compress'>"); + writer.write("<method>zlib</method></compress>"); + writer.flush(); + } + catch (IOException e) { + packetReader.notifyConnectionError(e); + } + } + + /** + * Start using stream compression since the server has acknowledged stream compression. + */ + void startStreamCompression() throws Exception { + // Secure the plain connection + usingCompression = true; + // Initialize the reader and writer with the new secured version + initReaderAndWriter(); + + // Set the new writer to use + packetWriter.setWriter(writer); + // Send a new opening stream to the server + packetWriter.openStream(); + // Notify that compression is being used + synchronized (this) { + this.notify(); + } + } + + void streamCompressionDenied() { + // Notify that compression has been denied + synchronized (this) { + this.notify(); + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPException.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPException.java new file mode 100644 index 000000000..5b60220ba --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/XMPPException.java @@ -0,0 +1,219 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack; + +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.packet.StreamError; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * A generic exception that is thrown when an error occurs performing an + * XMPP operation. XMPP servers can respond to error conditions with an error code + * and textual description of the problem, which are encapsulated in the XMPPError + * class. When appropriate, an XMPPError instance is attached instances of this exception.<p> + * + * When a stream error occured, the server will send a stream error to the client before + * closing the connection. Stream errors are unrecoverable errors. When a stream error + * is sent to the client an XMPPException will be thrown containing the StreamError sent + * by the server. + * + * @see XMPPError + * @author Matt Tucker + */ +public class XMPPException extends Exception { + + private StreamError streamError = null; + private XMPPError error = null; + private Throwable wrappedThrowable = null; + + /** + * Creates a new XMPPException. + */ + public XMPPException() { + super(); + } + + /** + * Creates a new XMPPException with a description of the exception. + * + * @param message description of the exception. + */ + public XMPPException(String message) { + super(message); + } + + /** + * Creates a new XMPPException with the Throwable that was the root cause of the + * exception. + * + * @param wrappedThrowable the root cause of the exception. + */ + public XMPPException(Throwable wrappedThrowable) { + super(); + this.wrappedThrowable = wrappedThrowable; + } + + /** + * Cretaes a new XMPPException with the stream error that was the root case of the + * exception. When a stream error is received from the server then the underlying + * TCP connection will be closed by the server. + * + * @param streamError the root cause of the exception. + */ + public XMPPException(StreamError streamError) { + super(); + this.streamError = streamError; + } + + /** + * Cretaes a new XMPPException with the XMPPError that was the root case of the + * exception. + * + * @param error the root cause of the exception. + */ + public XMPPException(XMPPError error) { + super(); + this.error = error; + } + + /** + * Creates a new XMPPException with a description of the exception and the + * Throwable that was the root cause of the exception. + * + * @param message a description of the exception. + * @param wrappedThrowable the root cause of the exception. + */ + public XMPPException(String message, Throwable wrappedThrowable) { + super(message); + this.wrappedThrowable = wrappedThrowable; + } + + /** + * Creates a new XMPPException with a description of the exception, an XMPPError, + * and the Throwable that was the root cause of the exception. + * + * @param message a description of the exception. + * @param error the root cause of the exception. + * @param wrappedThrowable the root cause of the exception. + */ + public XMPPException(String message, XMPPError error, Throwable wrappedThrowable) { + super(message); + this.error = error; + this.wrappedThrowable = wrappedThrowable; + } + + /** + * Creates a new XMPPException with a description of the exception and the + * XMPPException that was the root cause of the exception. + * + * @param message a description of the exception. + * @param error the root cause of the exception. + */ + public XMPPException(String message, XMPPError error) { + super(message); + this.error = error; + } + + /** + * Returns the XMPPError asscociated with this exception, or <tt>null</tt> if there + * isn't one. + * + * @return the XMPPError asscociated with this exception. + */ + public XMPPError getXMPPError() { + return error; + } + + /** + * Returns the StreamError asscociated with this exception, or <tt>null</tt> if there + * isn't one. The underlying TCP connection is closed by the server after sending the + * stream error to the client. + * + * @return the StreamError asscociated with this exception. + */ + public StreamError getStreamError() { + return streamError; + } + + /** + * Returns the Throwable asscociated with this exception, or <tt>null</tt> if there + * isn't one. + * + * @return the Throwable asscociated with this exception. + */ + public Throwable getWrappedThrowable() { + return wrappedThrowable; + } + + public void printStackTrace() { + printStackTrace(System.err); + } + + public void printStackTrace(PrintStream out) { + super.printStackTrace(out); + if (wrappedThrowable != null) { + out.println("Nested Exception: "); + wrappedThrowable.printStackTrace(out); + } + } + + public void printStackTrace(PrintWriter out) { + super.printStackTrace(out); + if (wrappedThrowable != null) { + out.println("Nested Exception: "); + wrappedThrowable.printStackTrace(out); + } + } + + public String getMessage() { + String msg = super.getMessage(); + // If the message was not set, but there is an XMPPError, return the + // XMPPError as the message. + if (msg == null && error != null) { + return error.toString(); + } + else if (msg == null && streamError != null) { + return streamError.toString(); + } + return msg; + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + String message = super.getMessage(); + if (message != null) { + buf.append(message).append(": "); + } + if (error != null) { + buf.append(error); + } + if (streamError != null) { + buf.append(streamError); + } + if (wrappedThrowable != null) { + buf.append("\n -- caused by: ").append(wrappedThrowable); + } + + return buf.toString(); + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/ConsoleDebugger.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/ConsoleDebugger.java new file mode 100644 index 000000000..a3757cb2a --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/ConsoleDebugger.java @@ -0,0 +1,161 @@ +package org.jivesoftware.smack.debugger; + +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.util.*; + +import java.io.Reader; +import java.io.Writer; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Very simple debugger that prints to the console (stdout) the sent and received stanzas. Use + * this debugger with caution since printing to the console is an expensive operation that may + * even block the thread since only one thread may print at a time.<p> + * <p/> + * It is possible to not only print the raw sent and received stanzas but also the interpreted + * packets by Smack. By default interpreted packets won't be printed. To enable this feature + * just change the <tt>printInterpreted</tt> static variable to <tt>true</tt>. + * + * @author Gaston Dombiak + */ +public class ConsoleDebugger implements SmackDebugger { + + public static boolean printInterpreted = false; + private SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm:ss aaa"); + + private XMPPConnection connection = null; + + private PacketListener listener = null; + private ConnectionListener connListener = null; + + private Writer writer; + private Reader reader; + private ReaderListener readerListener; + private WriterListener writerListener; + + public ConsoleDebugger(XMPPConnection connection, Writer writer, Reader reader) { + this.connection = connection; + this.writer = writer; + this.reader = reader; + createDebug(); + } + + /** + * Creates the listeners that will print in the console when new activity is detected. + */ + private void createDebug() { + // Create a special Reader that wraps the main Reader and logs data to the GUI. + ObservableReader debugReader = new ObservableReader(reader); + readerListener = new ReaderListener() { + public void read(String str) { + System.out.println( + dateFormatter.format(new Date()) + " RCV (" + connection.hashCode() + + "): " + + str); + } + }; + debugReader.addReaderListener(readerListener); + + // Create a special Writer that wraps the main Writer and logs data to the GUI. + ObservableWriter debugWriter = new ObservableWriter(writer); + writerListener = new WriterListener() { + public void write(String str) { + System.out.println( + dateFormatter.format(new Date()) + " SENT (" + connection.hashCode() + + "): " + + str); + } + }; + debugWriter.addWriterListener(writerListener); + + // Assign the reader/writer objects to use the debug versions. The packet reader + // and writer will use the debug versions when they are created. + reader = debugReader; + writer = debugWriter; + + // Create a thread that will listen for all incoming packets and write them to + // the GUI. This is what we call "interpreted" packet data, since it's the packet + // data as Smack sees it and not as it's coming in as raw XML. + listener = new PacketListener() { + public void processPacket(Packet packet) { + if (printInterpreted) { + System.out.println( + dateFormatter.format(new Date()) + " RCV PKT (" + + connection.hashCode() + + "): " + + packet.toXML()); + } + } + }; + + connListener = new ConnectionListener() { + public void connectionClosed() { + System.out.println( + dateFormatter.format(new Date()) + " Connection closed (" + + connection.hashCode() + + ")"); + } + + public void connectionClosedOnError(Exception e) { + System.out.println( + dateFormatter.format(new Date()) + + " Connection closed due to an exception (" + + connection.hashCode() + + ")"); + e.printStackTrace(); + } + }; + } + + public Reader newConnectionReader(Reader newReader) { + ((ObservableReader)reader).removeReaderListener(readerListener); + ObservableReader debugReader = new ObservableReader(newReader); + debugReader.addReaderListener(readerListener); + reader = debugReader; + return reader; + } + + public Writer newConnectionWriter(Writer newWriter) { + ((ObservableWriter)writer).removeWriterListener(writerListener); + ObservableWriter debugWriter = new ObservableWriter(newWriter); + debugWriter.addWriterListener(writerListener); + writer = debugWriter; + return writer; + } + + public void userHasLogged(String user) { + boolean isAnonymous = "".equals(StringUtils.parseName(user)); + String title = + "User logged (" + connection.hashCode() + "): " + + (isAnonymous ? "" : StringUtils.parseBareAddress(user)) + + "@" + + connection.getServiceName() + + ":" + + connection.getPort(); + title += "/" + StringUtils.parseResource(user); + System.out.println(title); + // Add the connection listener to the connection so that the debugger can be notified + // whenever the connection is closed. + connection.addConnectionListener(connListener); + } + + public Reader getReader() { + return reader; + } + + public Writer getWriter() { + return writer; + } + + public PacketListener getReaderListener() { + return listener; + } + + public PacketListener getWriterListener() { + return null; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/LiteDebugger.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/LiteDebugger.java new file mode 100644 index 000000000..3646121ce --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/LiteDebugger.java @@ -0,0 +1,336 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.debugger; + +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.event.*; +import java.io.*; + +import javax.swing.*; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smack.util.*; + +/** + * The LiteDebugger is a very simple debugger that allows to debug sent, received and + * interpreted messages. + * + * @author Gaston Dombiak + */ +public class LiteDebugger implements SmackDebugger { + + private static final String NEWLINE = "\n"; + + private JFrame frame = null; + private XMPPConnection connection = null; + + private PacketListener listener = null; + + private Writer writer; + private Reader reader; + private ReaderListener readerListener; + private WriterListener writerListener; + + public LiteDebugger(XMPPConnection connection, Writer writer, Reader reader) { + this.connection = connection; + this.writer = writer; + this.reader = reader; + createDebug(); + } + + /** + * Creates the debug process, which is a GUI window that displays XML traffic. + */ + private void createDebug() { + frame = new JFrame("Smack Debug Window -- " + connection.getServiceName() + ":" + + connection.getPort()); + + // Add listener for window closing event + frame.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent evt) { + rootWindowClosing(evt); + } + }); + + // We'll arrange the UI into four tabs. The first tab contains all data, the second + // client generated XML, the third server generated XML, and the fourth is packet + // data from the server as seen by Smack. + JTabbedPane tabbedPane = new JTabbedPane(); + + JPanel allPane = new JPanel(); + allPane.setLayout(new GridLayout(3, 1)); + tabbedPane.add("All", allPane); + + // Create UI elements for client generated XML traffic. + final JTextArea sentText1 = new JTextArea(); + final JTextArea sentText2 = new JTextArea(); + sentText1.setEditable(false); + sentText2.setEditable(false); + sentText1.setForeground(new Color(112, 3, 3)); + sentText2.setForeground(new Color(112, 3, 3)); + allPane.add(new JScrollPane(sentText1)); + tabbedPane.add("Sent", new JScrollPane(sentText2)); + + // Add pop-up menu. + JPopupMenu menu = new JPopupMenu(); + JMenuItem menuItem1 = new JMenuItem("Copy"); + menuItem1.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + // Get the clipboard + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + // Set the sent text as the new content of the clipboard + clipboard.setContents(new StringSelection(sentText1.getText()), null); + } + }); + + JMenuItem menuItem2 = new JMenuItem("Clear"); + menuItem2.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + sentText1.setText(""); + sentText2.setText(""); + } + }); + + // Add listener to the text area so the popup menu can come up. + MouseListener popupListener = new PopupListener(menu); + sentText1.addMouseListener(popupListener); + sentText2.addMouseListener(popupListener); + menu.add(menuItem1); + menu.add(menuItem2); + + // Create UI elements for server generated XML traffic. + final JTextArea receivedText1 = new JTextArea(); + final JTextArea receivedText2 = new JTextArea(); + receivedText1.setEditable(false); + receivedText2.setEditable(false); + receivedText1.setForeground(new Color(6, 76, 133)); + receivedText2.setForeground(new Color(6, 76, 133)); + allPane.add(new JScrollPane(receivedText1)); + tabbedPane.add("Received", new JScrollPane(receivedText2)); + + // Add pop-up menu. + menu = new JPopupMenu(); + menuItem1 = new JMenuItem("Copy"); + menuItem1.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + // Get the clipboard + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + // Set the sent text as the new content of the clipboard + clipboard.setContents(new StringSelection(receivedText1.getText()), null); + } + }); + + menuItem2 = new JMenuItem("Clear"); + menuItem2.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + receivedText1.setText(""); + receivedText2.setText(""); + } + }); + + // Add listener to the text area so the popup menu can come up. + popupListener = new PopupListener(menu); + receivedText1.addMouseListener(popupListener); + receivedText2.addMouseListener(popupListener); + menu.add(menuItem1); + menu.add(menuItem2); + + // Create UI elements for interpreted XML traffic. + final JTextArea interpretedText1 = new JTextArea(); + final JTextArea interpretedText2 = new JTextArea(); + interpretedText1.setEditable(false); + interpretedText2.setEditable(false); + interpretedText1.setForeground(new Color(1, 94, 35)); + interpretedText2.setForeground(new Color(1, 94, 35)); + allPane.add(new JScrollPane(interpretedText1)); + tabbedPane.add("Interpreted", new JScrollPane(interpretedText2)); + + // Add pop-up menu. + menu = new JPopupMenu(); + menuItem1 = new JMenuItem("Copy"); + menuItem1.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + // Get the clipboard + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + // Set the sent text as the new content of the clipboard + clipboard.setContents(new StringSelection(interpretedText1.getText()), null); + } + }); + + menuItem2 = new JMenuItem("Clear"); + menuItem2.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + interpretedText1.setText(""); + interpretedText2.setText(""); + } + }); + + // Add listener to the text area so the popup menu can come up. + popupListener = new PopupListener(menu); + interpretedText1.addMouseListener(popupListener); + interpretedText2.addMouseListener(popupListener); + menu.add(menuItem1); + menu.add(menuItem2); + + frame.getContentPane().add(tabbedPane); + + frame.setSize(550, 400); + frame.setVisible(true); + + // Create a special Reader that wraps the main Reader and logs data to the GUI. + ObservableReader debugReader = new ObservableReader(reader); + readerListener = new ReaderListener() { + public void read(String str) { + int index = str.lastIndexOf(">"); + if (index != -1) { + receivedText1.append(str.substring(0, index + 1)); + receivedText2.append(str.substring(0, index + 1)); + receivedText1.append(NEWLINE); + receivedText2.append(NEWLINE); + if (str.length() > index) { + receivedText1.append(str.substring(index + 1)); + receivedText2.append(str.substring(index + 1)); + } + } + else { + receivedText1.append(str); + receivedText2.append(str); + } + } + }; + debugReader.addReaderListener(readerListener); + + // Create a special Writer that wraps the main Writer and logs data to the GUI. + ObservableWriter debugWriter = new ObservableWriter(writer); + writerListener = new WriterListener() { + public void write(String str) { + sentText1.append(str); + sentText2.append(str); + if (str.endsWith(">")) { + sentText1.append(NEWLINE); + sentText2.append(NEWLINE); + } + } + }; + debugWriter.addWriterListener(writerListener); + + // Assign the reader/writer objects to use the debug versions. The packet reader + // and writer will use the debug versions when they are created. + reader = debugReader; + writer = debugWriter; + + // Create a thread that will listen for all incoming packets and write them to + // the GUI. This is what we call "interpreted" packet data, since it's the packet + // data as Smack sees it and not as it's coming in as raw XML. + listener = new PacketListener() { + public void processPacket(Packet packet) { + interpretedText1.append(packet.toXML()); + interpretedText2.append(packet.toXML()); + interpretedText1.append(NEWLINE); + interpretedText2.append(NEWLINE); + } + }; + } + + /** + * Notification that the root window is closing. Stop listening for received and + * transmitted packets. + * + * @param evt the event that indicates that the root window is closing + */ + public void rootWindowClosing(WindowEvent evt) { + connection.removePacketListener(listener); + ((ObservableReader)reader).removeReaderListener(readerListener); + ((ObservableWriter)writer).removeWriterListener(writerListener); + } + + /** + * Listens for debug window popup dialog events. + */ + private class PopupListener extends MouseAdapter { + JPopupMenu popup; + + PopupListener(JPopupMenu popupMenu) { + popup = popupMenu; + } + + public void mousePressed(MouseEvent e) { + maybeShowPopup(e); + } + + public void mouseReleased(MouseEvent e) { + maybeShowPopup(e); + } + + private void maybeShowPopup(MouseEvent e) { + if (e.isPopupTrigger()) { + popup.show(e.getComponent(), e.getX(), e.getY()); + } + } + } + + public Reader newConnectionReader(Reader newReader) { + ((ObservableReader)reader).removeReaderListener(readerListener); + ObservableReader debugReader = new ObservableReader(newReader); + debugReader.addReaderListener(readerListener); + reader = debugReader; + return reader; + } + + public Writer newConnectionWriter(Writer newWriter) { + ((ObservableWriter)writer).removeWriterListener(writerListener); + ObservableWriter debugWriter = new ObservableWriter(newWriter); + debugWriter.addWriterListener(writerListener); + writer = debugWriter; + return writer; + } + + public void userHasLogged(String user) { + boolean isAnonymous = "".equals(StringUtils.parseName(user)); + String title = + "Smack Debug Window -- " + + (isAnonymous ? "" : StringUtils.parseBareAddress(user)) + + "@" + + connection.getServiceName() + + ":" + + connection.getPort(); + title += "/" + StringUtils.parseResource(user); + frame.setTitle(title); + } + + public Reader getReader() { + return reader; + } + + public Writer getWriter() { + return writer; + } + + public PacketListener getReaderListener() { + return listener; + } + + public PacketListener getWriterListener() { + return null; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/SmackDebugger.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/SmackDebugger.java new file mode 100644 index 000000000..b1599ea06 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/SmackDebugger.java @@ -0,0 +1,98 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.debugger; + +import java.io.*; + +import org.jivesoftware.smack.*; + +/** + * Interface that allows for implementing classes to debug XML traffic. That is a GUI window that + * displays XML traffic.<p> + * + * Every implementation of this interface <b>must</b> have a public constructor with the following + * arguments: XMPPConnection, Writer, Reader. + * + * @author Gaston Dombiak + */ +public interface SmackDebugger { + + /** + * Called when a user has logged in to the server. The user could be an anonymous user, this + * means that the user would be of the form host/resource instead of the form + * user@host/resource. + * + * @param user the user@host/resource that has just logged in + */ + public abstract void userHasLogged(String user); + + /** + * Returns the special Reader that wraps the main Reader and logs data to the GUI. + * + * @return the special Reader that wraps the main Reader and logs data to the GUI. + */ + public abstract Reader getReader(); + + /** + * Returns the special Writer that wraps the main Writer and logs data to the GUI. + * + * @return the special Writer that wraps the main Writer and logs data to the GUI. + */ + public abstract Writer getWriter(); + + /** + * Returns a new special Reader that wraps the new connection Reader. The connection + * has been secured so the connection is using a new reader and writer. The debugger + * needs to wrap the new reader and writer to keep being notified of the connection + * traffic. + * + * @return a new special Reader that wraps the new connection Reader. + */ + public abstract Reader newConnectionReader(Reader reader); + + /** + * Returns a new special Writer that wraps the new connection Writer. The connection + * has been secured so the connection is using a new reader and writer. The debugger + * needs to wrap the new reader and writer to keep being notified of the connection + * traffic. + * + * @return a new special Writer that wraps the new connection Writer. + */ + public abstract Writer newConnectionWriter(Writer writer); + + /** + * Returns the thread that will listen for all incoming packets and write them to the GUI. + * This is what we call "interpreted" packet data, since it's the packet data as Smack sees + * it and not as it's coming in as raw XML. + * + * @return the PacketListener that will listen for all incoming packets and write them to + * the GUI + */ + public abstract PacketListener getReaderListener(); + + /** + * Returns the thread that will listen for all outgoing packets and write them to the GUI. + * + * @return the PacketListener that will listen for all sent packets and write them to + * the GUI + */ + public abstract PacketListener getWriterListener(); +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/package.html new file mode 100644 index 000000000..afb861f5e --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/debugger/package.html @@ -0,0 +1 @@ +<body>Core debugger functionality.</body>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/AndFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/AndFilter.java new file mode 100644 index 000000000..2ca4a1c2b --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/AndFilter.java @@ -0,0 +1,103 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Packet; + +/** + * Implements the logical AND operation over two or more packet filters. + * In other words, packets pass this filter if they pass <b>all</b> of the filters. + * + * @author Matt Tucker + */ +public class AndFilter implements PacketFilter { + + /** + * The current number of elements in the filter. + */ + private int size; + + /** + * The list of filters. + */ + private PacketFilter [] filters; + + /** + * Creates an empty AND filter. Filters should be added using the + * {@link #addFilter(PacketFilter)} method. + */ + public AndFilter() { + size = 0; + filters = new PacketFilter[3]; + } + + /** + * Creates an AND filter using the two specified filters. + * + * @param filter1 the first packet filter. + * @param filter2 the second packet filter. + */ + public AndFilter(PacketFilter filter1, PacketFilter filter2) { + if (filter1 == null || filter2 == null) { + throw new IllegalArgumentException("Parameters cannot be null."); + } + size = 2; + filters = new PacketFilter[2]; + filters[0] = filter1; + filters[1] = filter2; + } + + /** + * Adds a filter to the filter list for the AND operation. A packet + * will pass the filter if all of the filters in the list accept it. + * + * @param filter a filter to add to the filter list. + */ + public void addFilter(PacketFilter filter) { + if (filter == null) { + throw new IllegalArgumentException("Parameter cannot be null."); + } + // If there is no more room left in the filters array, expand it. + if (size == filters.length) { + PacketFilter [] newFilters = new PacketFilter[filters.length+2]; + for (int i=0; i<filters.length; i++) { + newFilters[i] = filters[i]; + } + filters = newFilters; + } + // Add the new filter to the array. + filters[size] = filter; + size++; + } + + public boolean accept(Packet packet) { + for (int i=0; i<size; i++) { + if (!filters[i].accept(packet)) { + return false; + } + } + return true; + } + + public String toString() { + return filters.toString(); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromContainsFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromContainsFilter.java new file mode 100644 index 000000000..7b5862184 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromContainsFilter.java @@ -0,0 +1,54 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Packet; + +/** + * Filters for packets where the "from" field contains a specified value. + * + * @author Matt Tucker + */ +public class FromContainsFilter implements PacketFilter { + + private String from; + + /** + * Creates a "from" contains filter using the "from" field part. + * + * @param from the from field value the packet must contain. + */ + public FromContainsFilter(String from) { + if (from == null) { + throw new IllegalArgumentException("Parameter cannot be null."); + } + this.from = from.toLowerCase(); + } + + public boolean accept(Packet packet) { + if (packet.getFrom() == null) { + return false; + } + else { + return packet.getFrom().toLowerCase().indexOf(from) != -1; + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromMatchesFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromMatchesFilter.java new file mode 100644 index 000000000..1562d5cac --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/FromMatchesFilter.java @@ -0,0 +1,71 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.util.StringUtils; + +/** + * Filter for packets where the "from" field exactly matches a specified JID. If the specified + * address is a bare JID then the filter will match any address whose bare JID matches the + * specified JID. But if the specified address is a full JID then the filter will only match + * if the sender of the packet matches the specified resource. + * + * @author Gaston Dombiak + */ +public class FromMatchesFilter implements PacketFilter { + + private String address; + /** + * Flag that indicates if the checking will be done against bare JID addresses or full JIDs. + */ + private boolean matchBareJID = false; + + /** + * Creates a "from" filter using the "from" field part. If the specified address is a bare JID + * then the filter will match any address whose bare JID matches the specified JID. But if the + * specified address is a full JID then the filter will only match if the sender of the packet + * matches the specified resource. + * + * @param address the from field value the packet must match. Could be a full or bare JID. + */ + public FromMatchesFilter(String address) { + if (address == null) { + throw new IllegalArgumentException("Parameter cannot be null."); + } + this.address = address.toLowerCase(); + matchBareJID = "".equals(StringUtils.parseResource(address)); + } + + public boolean accept(Packet packet) { + if (packet.getFrom() == null) { + return false; + } + else if (matchBareJID) { + // Check if the bare JID of the sender of the packet matches the specified JID + return packet.getFrom().toLowerCase().startsWith(address); + } + else { + // Check if the full JID of the sender of the packet matches the specified JID + return address.equals(packet.getFrom().toLowerCase()); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/IQTypeFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/IQTypeFilter.java new file mode 100644 index 000000000..efe600307 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/IQTypeFilter.java @@ -0,0 +1,48 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; + +/** + * A filter for IQ packet types. Returns true only if the packet is an IQ packet + * and it matches the type provided in the constructor. + * + * @author Alexander Wenckus + * + */ +public class IQTypeFilter implements PacketFilter { + + private IQ.Type type; + + public IQTypeFilter(IQ.Type type) { + this.type = type; + } + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smack.filter.PacketFilter#accept(org.jivesoftware.smack.packet.Packet) + */ + public boolean accept(Packet packet) { + return (packet instanceof IQ && ((IQ) packet).getType().equals(type)); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/MessageTypeFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/MessageTypeFilter.java new file mode 100644 index 000000000..618ca6793 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/MessageTypeFilter.java @@ -0,0 +1,54 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; + +/** + * Filters for packets of a specific type of Message (e.g. CHAT). + * + * @see org.jivesoftware.smack.packet.Message.Type + * @author Ward Harold + */ +public class MessageTypeFilter implements PacketFilter { + + private final Message.Type type; + + /** + * Creates a new message type filter using the specified message type. + * + * @param type the message type. + */ + public MessageTypeFilter(Message.Type type) { + this.type = type; + } + + public boolean accept(Packet packet) { + if (!(packet instanceof Message)) { + return false; + } + else { + return ((Message) packet).getType().equals(this.type); + } + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/NotFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/NotFilter.java new file mode 100644 index 000000000..4e6e5494c --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/NotFilter.java @@ -0,0 +1,50 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Packet; + +/** + * Implements the logical NOT operation on a packet filter. In other words, packets + * pass this filter if they do not pass the supplied filter. + * + * @author Matt Tucker + */ +public class NotFilter implements PacketFilter { + + private PacketFilter filter; + + /** + * Creates a NOT filter using the specified filter. + * + * @param filter the filter. + */ + public NotFilter(PacketFilter filter) { + if (filter == null) { + throw new IllegalArgumentException("Parameter cannot be null."); + } + this.filter = filter; + } + + public boolean accept(Packet packet) { + return !filter.accept(packet); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/OrFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/OrFilter.java new file mode 100644 index 000000000..22c3d91af --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/OrFilter.java @@ -0,0 +1,103 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Packet; + +/** + * Implements the logical OR operation over two or more packet filters. In + * other words, packets pass this filter if they pass <b>any</b> of the filters. + * + * @author Matt Tucker + */ +public class OrFilter implements PacketFilter { + + /** + * The current number of elements in the filter. + */ + private int size; + + /** + * The list of filters. + */ + private PacketFilter [] filters; + + /** + * Creates an empty OR filter. Filters should be added using the + * {@link #addFilter(PacketFilter)} method. + */ + public OrFilter() { + size = 0; + filters = new PacketFilter[3]; + } + + /** + * Creates an OR filter using the two specified filters. + * + * @param filter1 the first packet filter. + * @param filter2 the second packet filter. + */ + public OrFilter(PacketFilter filter1, PacketFilter filter2) { + if (filter1 == null || filter2 == null) { + throw new IllegalArgumentException("Parameters cannot be null."); + } + size = 2; + filters = new PacketFilter[2]; + filters[0] = filter1; + filters[1] = filter2; + } + + /** + * Adds a filter to the filter list for the OR operation. A packet + * will pass the filter if any filter in the list accepts it. + * + * @param filter a filter to add to the filter list. + */ + public void addFilter(PacketFilter filter) { + if (filter == null) { + throw new IllegalArgumentException("Parameter cannot be null."); + } + // If there is no more room left in the filters array, expand it. + if (size == filters.length) { + PacketFilter [] newFilters = new PacketFilter[filters.length+2]; + for (int i=0; i<filters.length; i++) { + newFilters[i] = filters[i]; + } + filters = newFilters; + } + // Add the new filter to the array. + filters[size] = filter; + size++; + } + + public boolean accept(Packet packet) { + for (int i=0; i<size; i++) { + if (filters[i].accept(packet)) { + return true; + } + } + return false; + } + + public String toString() { + return filters.toString(); + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketExtensionFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketExtensionFilter.java new file mode 100644 index 000000000..b46c118cc --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketExtensionFilter.java @@ -0,0 +1,51 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Packet; + +/** + * Filters for packets with a particular type of packet extension. + * + * @author Matt Tucker + */ +public class PacketExtensionFilter implements PacketFilter { + + private String elementName; + private String namespace; + + /** + * Creates a new packet extension filter. Packets will pass the filter if + * they have a packet extension that matches the specified element name + * and namespace. + * + * @param elementName the XML element name of the packet extension. + * @param namespace the XML namespace of the packet extension. + */ + public PacketExtensionFilter(String elementName, String namespace) { + this.elementName = elementName; + this.namespace = namespace; + } + + public boolean accept(Packet packet) { + return packet.getExtension(elementName, namespace) != null; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketFilter.java new file mode 100644 index 000000000..fbdcac5a1 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketFilter.java @@ -0,0 +1,63 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Packet; + +/** + * Defines a way to filter packets for particular attributes. Packet filters are + * used when constructing packet listeners or collectors -- the filter defines + * what packets match the criteria of the collector or listener for further + * packet processing.<p> + * + * Several pre-defined filters are defined. These filters can be logically combined + * for more complex packet filtering by using the + * {@link org.jivesoftware.smack.filter.AndFilter AndFilter} and + * {@link org.jivesoftware.smack.filter.OrFilter OrFilter} filters. It's also possible + * to define your own filters by implementing this interface. The code example below + * creates a trivial filter for packets with a specific ID. + * + * <pre> + * // Use an anonymous inner class to define a packet filter that returns + * // all packets that have a packet ID of "RS145". + * PacketFilter myFilter = new PacketFilter() { + * public boolean accept(Packet packet) { + * return "RS145".equals(packet.getPacketID()); + * } + * }; + * // Create a new packet collector using the filter we created. + * PacketCollector myCollector = packetReader.createPacketCollector(myFilter); + * </pre> + * + * @see org.jivesoftware.smack.PacketCollector + * @see org.jivesoftware.smack.PacketListener + * @author Matt Tucker + */ +public interface PacketFilter { + + /** + * Tests whether or not the specified packet should pass the filter. + * + * @param packet the packet to test. + * @return true if and only if <tt>packet</tt> passes the filter. + */ + public boolean accept(Packet packet); +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketIDFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketIDFilter.java new file mode 100644 index 000000000..03a007f37 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketIDFilter.java @@ -0,0 +1,49 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Packet; + +/** + * Filters for packets with a particular packet ID. + * + * @author Matt Tucker + */ +public class PacketIDFilter implements PacketFilter { + + private String packetID; + + /** + * Creates a new packet ID filter using the specified packet ID. + * + * @param packetID the packet ID to filter for. + */ + public PacketIDFilter(String packetID) { + if (packetID == null) { + throw new IllegalArgumentException("Packet ID cannot be null."); + } + this.packetID = packetID; + } + + public boolean accept(Packet packet) { + return packetID.equals(packet.getPacketID()); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketTypeFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketTypeFilter.java new file mode 100644 index 000000000..1a736e114 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/PacketTypeFilter.java @@ -0,0 +1,58 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Packet; + +/** + * Filters for packets of a particular type. The type is given as a Class object, so + * example types would: + * <ul> + * <li><tt>Message.class</tt> + * <li><tt>IQ.class</tt> + * <li><tt>Presence.class</tt> + * </ul> + * + * @author Matt Tucker + */ +public class PacketTypeFilter implements PacketFilter { + + Class packetType; + + /** + * Creates a new packet type filter that will filter for packets that are the + * same type as <tt>packetType</tt>. + * + * @param packetType the Class type. + */ + public PacketTypeFilter(Class packetType) { + // Ensure the packet type is a sub-class of Packet. + if (!Packet.class.isAssignableFrom(packetType)) { + throw new IllegalArgumentException("Packet type must be a sub-class of Packet."); + } + this.packetType = packetType; + } + + public boolean accept(Packet packet) { + return packetType.isInstance(packet); + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ThreadFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ThreadFilter.java new file mode 100644 index 000000000..b9c296d11 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ThreadFilter.java @@ -0,0 +1,55 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.Message; + +/** + * Filters for message packets with a particular thread value. + * + * @author Matt Tucker + */ +public class ThreadFilter implements PacketFilter { + + private String thread; + + /** + * Creates a new thread filter using the specified thread value. + * + * @param thread the thread value to filter for. + */ + public ThreadFilter(String thread) { + if (thread == null) { + throw new IllegalArgumentException("Thread cannot be null."); + } + this.thread = thread; + } + + public boolean accept(Packet packet) { + if (packet instanceof Message) { + return thread.equals(((Message)packet).getThread()); + } + else { + return false; + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ToContainsFilter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ToContainsFilter.java new file mode 100644 index 000000000..1ec8a8a05 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/ToContainsFilter.java @@ -0,0 +1,55 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.filter; + +import org.jivesoftware.smack.packet.Packet; + +/** + * Filters for packets where the "to" field contains a specified value. For example, + * the filter could be used to listen for all packets sent to a group chat nickname. + * + * @author Matt Tucker + */ +public class ToContainsFilter implements PacketFilter { + + private String to; + + /** + * Creates a "to" contains filter using the "to" field part. + * + * @param to the to field value the packet must contain. + */ + public ToContainsFilter(String to) { + if (to == null) { + throw new IllegalArgumentException("Parameter cannot be null."); + } + this.to = to.toLowerCase(); + } + + public boolean accept(Packet packet) { + if (packet.getTo() == null) { + return false; + } + else { + return packet.getTo().toLowerCase().indexOf(to) != -1; + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/package.html new file mode 100644 index 000000000..8b3fe8034 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/filter/package.html @@ -0,0 +1 @@ +<body>Allows {@link org.jivesoftware.smack.PacketCollector} and {@link org.jivesoftware.smack.PacketListener} instances to filter for packets with particular attributes.</body>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/package.html new file mode 100644 index 000000000..2758d781a --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/package.html @@ -0,0 +1 @@ +<body>Core classes of the Smack API.</body>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Authentication.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Authentication.java new file mode 100644 index 000000000..dce690b40 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Authentication.java @@ -0,0 +1,186 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.packet; + +import org.jivesoftware.smack.util.StringUtils; + +/** + * Authentication packet, which can be used to login to a XMPP server as well + * as discover login information from the server. + */ +public class Authentication extends IQ { + + private String username = null; + private String password = null; + private String digest = null; + private String resource = null; + + /** + * Create a new authentication packet. By default, the packet will be in + * "set" mode in order to perform an actual authentication with the server. + * In order to send a "get" request to get the available authentication + * modes back from the server, change the type of the IQ packet to "get": + * <p/> + * <p><tt>setType(IQ.Type.GET);</tt> + */ + public Authentication() { + setType(IQ.Type.SET); + } + + /** + * Returns the username, or <tt>null</tt> if the username hasn't been sent. + * + * @return the username. + */ + public String getUsername() { + return username; + } + + /** + * Sets the username. + * + * @param username the username. + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Returns the plain text password or <tt>null</tt> if the password hasn't + * been set. + * + * @return the password. + */ + public String getPassword() { + return password; + } + + /** + * Sets the plain text password. + * + * @param password the password. + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * Returns the password digest or <tt>null</tt> if the digest hasn't + * been set. Password digests offer a more secure alternative for + * authentication compared to plain text. The digest is the hex-encoded + * SHA-1 hash of the connection ID plus the user's password. If the + * digest and password are set, digest authentication will be used. If + * only one value is set, the respective authentication mode will be used. + * + * @return the digest of the user's password. + */ + public String getDigest() { + return digest; + } + + /** + * Sets the digest value using a connection ID and password. Password + * digests offer a more secure alternative for authentication compared to + * plain text. The digest is the hex-encoded SHA-1 hash of the connection ID + * plus the user's password. If the digest and password are set, digest + * authentication will be used. If only one value is set, the respective + * authentication mode will be used. + * + * @param connectionID the connection ID. + * @param password the password. + * @see org.jivesoftware.smack.XMPPConnection#getConnectionID() + */ + public void setDigest(String connectionID, String password) { + this.digest = StringUtils.hash(connectionID + password); + } + + /** + * Sets the digest value directly. Password digests offer a more secure + * alternative for authentication compared to plain text. The digest is + * the hex-encoded SHA-1 hash of the connection ID plus the user's password. + * If the digest and password are set, digest authentication will be used. + * If only one value is set, the respective authentication mode will be used. + * + * @param digest the digest, which is the SHA-1 hash of the connection ID + * the user's password, encoded as hex. + * @see org.jivesoftware.smack.XMPPConnection#getConnectionID() + */ + public void setDigest(String digest) { + this.digest = digest; + } + + /** + * Returns the resource or <tt>null</tt> if the resource hasn't been set. + * + * @return the resource. + */ + public String getResource() { + return resource; + } + + /** + * Sets the resource. + * + * @param resource the resource. + */ + public void setResource(String resource) { + this.resource = resource; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"jabber:iq:auth\">"); + if (username != null) { + if (username.equals("")) { + buf.append("<username/>"); + } + else { + buf.append("<username>").append(username).append("</username>"); + } + } + if (digest != null) { + if (digest.equals("")) { + buf.append("<digest/>"); + } + else { + buf.append("<digest>").append(digest).append("</digest>"); + } + } + if (password != null && digest == null) { + if (password.equals("")) { + buf.append("<password/>"); + } + else { + buf.append("<password>").append(StringUtils.escapeForXML(password)).append("</password>"); + } + } + if (resource != null) { + if (resource.equals("")) { + buf.append("<resource/>"); + } + else { + buf.append("<resource>").append(resource).append("</resource>"); + } + } + buf.append("</query>"); + return buf.toString(); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Bind.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Bind.java new file mode 100644 index 000000000..5646ecbca --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Bind.java @@ -0,0 +1,71 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.packet; + +/** + * IQ packet used by Smack to bind a resource and to obtain the jid assigned by the server. + * There are two ways to bind a resource. One is simply sending an empty Bind packet where the + * server will assign a new resource for this connection. The other option is to set a desired + * resource but the server may return a modified version of the sent resource.<p> + * + * For more information refer to the following + * <a href=http://www.xmpp.org/specs/rfc3920.html#bind>link</a>. + * + * @author Gaston Dombiak + */ +public class Bind extends IQ { + + private String resource = null; + private String jid = null; + + public Bind() { + setType(IQ.Type.SET); + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getJid() { + return jid; + } + + public void setJid(String jid) { + this.jid = jid; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\">"); + if (resource != null) { + buf.append("<resource>").append(resource).append("</resource>"); + } + if (jid != null) { + buf.append("<jid>").append(jid).append("</jid>"); + } + buf.append("</bind>"); + return buf.toString(); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/DefaultPacketExtension.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/DefaultPacketExtension.java new file mode 100644 index 000000000..cbf1b5efe --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/DefaultPacketExtension.java @@ -0,0 +1,134 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.packet; + +import java.util.*; + +/** + * Default implementation of the PacketExtension interface. Unless a PacketExtensionProvider + * is registered with {@link org.jivesoftware.smack.provider.ProviderManager ProviderManager}, + * instances of this class will be returned when getting packet extensions.<p> + * + * This class provides a very simple representation of an XML sub-document. Each element + * is a key in a Map with its CDATA being the value. For example, given the following + * XML sub-document: + * + * <pre> + * <foo xmlns="http://bar.com"> + * <color>blue</color> + * <food>pizza</food> + * </foo></pre> + * + * In this case, getValue("color") would return "blue", and getValue("food") would + * return "pizza". This parsing mechanism mechanism is very simplistic and will not work + * as desired in all cases (for example, if some of the elements have attributes. In those + * cases, a custom PacketExtensionProvider should be used. + * + * @author Matt Tucker + */ +public class DefaultPacketExtension implements PacketExtension { + + private String elementName; + private String namespace; + private Map map; + + /** + * Creates a new generic packet extension. + * + * @param elementName the name of the element of the XML sub-document. + * @param namespace the namespace of the element. + */ + public DefaultPacketExtension(String elementName, String namespace) { + this.elementName = elementName; + this.namespace = namespace; + } + + /** + * Returns the XML element name of the extension sub-packet root element. + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return elementName; + } + + /** + * Returns the XML namespace of the extension sub-packet root element. + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return namespace; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\">"); + for (Iterator i=getNames(); i.hasNext(); ) { + String name = (String)i.next(); + String value = getValue(name); + buf.append("<").append(name).append(">"); + buf.append(value); + buf.append("</").append(name).append(">"); + } + buf.append("</").append(elementName).append(">"); + return buf.toString(); + } + + /** + * Returns an Iterator for the names that can be used to get + * values of the packet extension. + * + * @return an Iterator for the names. + */ + public synchronized Iterator getNames() { + if (map == null) { + return Collections.EMPTY_LIST.iterator(); + } + return Collections.unmodifiableMap(new HashMap(map)).keySet().iterator(); + } + + /** + * Returns a packet extension value given a name. + * + * @param name the name. + * @return the value. + */ + public synchronized String getValue(String name) { + if (map == null) { + return null; + } + return (String)map.get(name); + } + + /** + * Sets a packet extension value using the given name. + * + * @param name the name. + * @param value the value. + */ + public synchronized void setValue(String name, String value) { + if (map == null) { + map = new HashMap(); + } + map.put(name, value); + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/IQ.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/IQ.java new file mode 100644 index 000000000..926e4e422 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/IQ.java @@ -0,0 +1,167 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.packet; + +import org.jivesoftware.smack.util.StringUtils; + +/** + * The base IQ (Info/Query) packet. IQ packets are used to get and set information + * on the server, including authentication, roster operations, and creating + * accounts. Each IQ packet has a specific type that indicates what type of action + * is being taken: "get", "set", "result", or "error".<p> + * + * IQ packets can contain a single child element that exists in a specific XML + * namespace. The combination of the element name and namespace determines what + * type of IQ packet it is. Some example IQ subpacket snippets:<ul> + * + * <li><query xmlns="jabber:iq:auth"> -- an authentication IQ. + * <li><query xmlns="jabber:iq:private"> -- a private storage IQ. + * <li><pubsub xmlns="http://jabber.org/protocol/pubsub"> -- a pubsub IQ. + * </ul> + * + * @author Matt Tucker + */ +public abstract class IQ extends Packet { + + private Type type = Type.GET; + + /** + * Returns the type of the IQ packet. + * + * @return the type of the IQ packet. + */ + public Type getType() { + return type; + } + + /** + * Sets the type of the IQ packet. + * + * @param type the type of the IQ packet. + */ + public void setType(Type type) { + if (type == null) { + this.type = Type.GET; + } + else { + this.type = type; + } + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<iq "); + if (getPacketID() != null) { + buf.append("id=\"" + getPacketID() + "\" "); + } + if (getTo() != null) { + buf.append("to=\"").append(StringUtils.escapeForXML(getTo())).append("\" "); + } + if (getFrom() != null) { + buf.append("from=\"").append(StringUtils.escapeForXML(getFrom())).append("\" "); + } + if (type == null) { + buf.append("type=\"get\">"); + } + else { + buf.append("type=\"").append(getType()).append("\">"); + } + // Add the query section if there is one. + String queryXML = getChildElementXML(); + if (queryXML != null) { + buf.append(queryXML); + } + // Add the error sub-packet, if there is one. + XMPPError error = getError(); + if (error != null) { + buf.append(error.toXML()); + } + buf.append("</iq>"); + return buf.toString(); + } + + /** + * Returns the sub-element XML section of the IQ packet, or <tt>null</tt> if there + * isn't one. Packet extensions <b>must</b> be included, if any are defined.<p> + * + * Extensions of this class must override this method. + * + * @return the child element section of the IQ XML. + */ + public abstract String getChildElementXML(); + + /** + * A class to represent the type of the IQ packet. The types are: + * + * <ul> + * <li>IQ.Type.GET + * <li>IQ.Type.SET + * <li>IQ.Type.RESULT + * <li>IQ.Type.ERROR + * </ul> + */ + public static class Type { + + public static final Type GET = new Type("get"); + public static final Type SET = new Type("set"); + public static final Type RESULT = new Type("result"); + public static final Type ERROR = new Type("error"); + + /** + * Converts a String into the corresponding types. Valid String values + * that can be converted to types are: "get", "set", "result", and "error". + * + * @param type the String value to covert. + * @return the corresponding Type. + */ + public static Type fromString(String type) { + if (type == null) { + return null; + } + type = type.toLowerCase(); + if (GET.toString().equals(type)) { + return GET; + } + else if (SET.toString().equals(type)) { + return SET; + } + else if (ERROR.toString().equals(type)) { + return ERROR; + } + else if (RESULT.toString().equals(type)) { + return RESULT; + } + else { + return null; + } + } + + private String value; + + private Type(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Message.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Message.java new file mode 100644 index 000000000..193b24d1f --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Message.java @@ -0,0 +1,273 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.packet; + +import org.jivesoftware.smack.util.StringUtils; + +/** + * Represents XMPP message packets. A message can be one of several types: + * + * <ul> + * <li>Message.Type.NORMAL -- (Default) a normal text message used in email like interface. + * <li>Message.Type.CHAT -- a typically short text message used in line-by-line chat interfaces. + * <li>Message.Type.GROUP_CHAT -- a chat message sent to a groupchat server for group chats. + * <li>Message.Type.HEADLINE -- a text message to be displayed in scrolling marquee displays. + * <li>Message.Type.ERROR -- indicates a messaging error. + * </ul> + * + * For each message type, different message fields are typically used as follows: + * <p> + * <table border="1"> + * <tr><td> </td><td colspan="5"><b>Message type</b></td></tr> + * <tr><td><i>Field</i></td><td><b>Normal</b></td><td><b>Chat</b></td><td><b>Group Chat</b></td><td><b>Headline</b></td><td><b>XMPPError</b></td></tr> + * <tr><td><i>subject</i></td> <td>SHOULD</td><td>SHOULD NOT</td><td>SHOULD NOT</td><td>SHOULD NOT</td><td>SHOULD NOT</td></tr> + * <tr><td><i>thread</i></td> <td>OPTIONAL</td><td>SHOULD</td><td>OPTIONAL</td><td>OPTIONAL</td><td>SHOULD NOT</td></tr> + * <tr><td><i>body</i></td> <td>SHOULD</td><td>SHOULD</td><td>SHOULD</td><td>SHOULD</td><td>SHOULD NOT</td></tr> + * <tr><td><i>error</i></td> <td>MUST NOT</td><td>MUST NOT</td><td>MUST NOT</td><td>MUST NOT</td><td>MUST</td></tr> + * </table> + * + * @author Matt Tucker + */ +public class Message extends Packet { + + private Type type = Type.NORMAL; + private String subject = null; + private String body = null; + private String thread = null; + + /** + * Creates a new, "normal" message. + */ + public Message() { + } + + /** + * Creates a new "normal" message to the specified recipient. + * + * @param to the recipient of the message. + */ + public Message(String to) { + if (to == null) { + throw new IllegalArgumentException("Parameter cannot be null"); + } + setTo(to); + } + + /** + * Creates a new message of the specified type to a recipient. + * + * @param to the user to send the message to. + * @param type the message type. + */ + public Message(String to, Type type) { + if (to == null || type == null) { + throw new IllegalArgumentException("Parameters cannot be null."); + } + setTo(to); + this.type = type; + } + + /** + * Returns the type of the message. + * + * @return the type of the message. + */ + public Type getType() { + return type; + } + + /** + * Sets the type of the message. + * + * @param type the type of the message. + */ + public void setType(Type type) { + if (type == null) { + throw new IllegalArgumentException("Type cannot be null."); + } + this.type = type; + } + + /** + * Returns the subject of the message, or null if the subject has not been set. + * The subject is a short description of message contents. + * + * @return the subject of the message. + */ + public String getSubject() { + return subject; + } + + /** + * Sets the subject of the message. The subject is a short description of + * message contents. + * + * @param subject the subject of the message. + */ + public void setSubject(String subject) { + this.subject = subject; + } + + /** + * Returns the body of the message, or null if the body has not been set. The body + * is the main message contents. + * + * @return the body of the message. + */ + public String getBody() { + return body; + } + + /** + * Sets the body of the message. The body is the main message contents. + * @param body + */ + public void setBody(String body) { + this.body = body; + } + + /** + * Returns the thread id of the message, which is a unique identifier for a sequence + * of "chat" messages. If no thread id is set, <tt>null</tt> will be returned. + * + * @return the thread id of the message, or <tt>null</tt> if it doesn't exist. + */ + public String getThread() { + return thread; + } + + /** + * Sets the thread id of the message, which is a unique identifier for a sequence + * of "chat" messages. + * + * @param thread the thread id of the message. + */ + public void setThread(String thread) { + this.thread = thread; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<message"); + if (getPacketID() != null) { + buf.append(" id=\"").append(getPacketID()).append("\""); + } + if (getTo() != null) { + buf.append(" to=\"").append(StringUtils.escapeForXML(getTo())).append("\""); + } + if (getFrom() != null) { + buf.append(" from=\"").append(StringUtils.escapeForXML(getFrom())).append("\""); + } + if (type != Type.NORMAL) { + buf.append(" type=\"").append(type).append("\""); + } + buf.append(">"); + if (subject != null) { + buf.append("<subject>").append(StringUtils.escapeForXML(subject)).append("</subject>"); + } + if (body != null) { + buf.append("<body>").append(StringUtils.escapeForXML(body)).append("</body>"); + } + if (thread != null) { + buf.append("<thread>").append(thread).append("</thread>"); + } + // Append the error subpacket if the message type is an error. + if (type == Type.ERROR) { + XMPPError error = getError(); + if (error != null) { + buf.append(error.toXML()); + } + } + // Add packet extensions, if any are defined. + buf.append(getExtensionsXML()); + buf.append("</message>"); + return buf.toString(); + } + + /** + * Represents the type of a message. + */ + public static class Type { + + /** + * (Default) a normal text message used in email like interface. + */ + public static final Type NORMAL = new Type("normal"); + + /** + * Typically short text message used in line-by-line chat interfaces. + */ + public static final Type CHAT = new Type("chat"); + + /** + * Chat message sent to a groupchat server for group chats. + */ + public static final Type GROUP_CHAT = new Type("groupchat"); + + /** + * Text message to be displayed in scrolling marquee displays. + */ + public static final Type HEADLINE = new Type("headline"); + + /** + * indicates a messaging error. + */ + public static final Type ERROR = new Type("error"); + + /** + * Converts a String value into its Type representation. + * + * @param type the String value. + * @return the Type corresponding to the String. + */ + public static Type fromString(String type) { + if (type == null) { + return NORMAL; + } + type = type.toLowerCase(); + if (CHAT.toString().equals(type)) { + return CHAT; + } + else if (GROUP_CHAT.toString().equals(type)) { + return GROUP_CHAT; + } + else if (HEADLINE.toString().equals(type)) { + return HEADLINE; + } + else if (ERROR.toString().equals(type)) { + return ERROR; + } + else { + return NORMAL; + } + } + + private String value; + + private Type(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Packet.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Packet.java new file mode 100644 index 000000000..a0de7a602 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Packet.java @@ -0,0 +1,423 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.packet; + +import org.jivesoftware.smack.util.StringUtils; + +import java.util.*; +import java.io.*; + +/** + * Base class for XMPP packets. Every packet has a unique ID (which is automatically + * generated, but can be overriden). Optionally, the "to" and "from" fields can be set, + * as well as an arbitrary number of properties. + * + * Properties provide an easy mechanism for clients to share data. Each property has a + * String name, and a value that is a Java primitive (int, long, float, double, boolean) + * or any Serializable object (a Java object is Serializable when it implements the + * Serializable interface). + * + * @author Matt Tucker + */ +public abstract class Packet { + + /** + * Constant used as packetID to indicate that a packet has no id. To indicate that a packet + * has no id set this constant as the packet's id. When the packet is asked for its id the + * answer will be <tt>null</tt>. + */ + public static final String ID_NOT_AVAILABLE = "ID_NOT_AVAILABLE"; + + /** + * A prefix helps to make sure that ID's are unique across mutliple instances. + */ + private static String prefix = StringUtils.randomString(5) + "-"; + + /** + * Keeps track of the current increment, which is appended to the prefix to + * forum a unique ID. + */ + private static long id = 0; + + /** + * Returns the next unique id. Each id made up of a short alphanumeric + * prefix along with a unique numeric value. + * + * @return the next id. + */ + private static synchronized String nextID() { + return prefix + Long.toString(id++); + } + + private String packetID = null; + private String to = null; + private String from = null; + private List packetExtensions = null; + private Map properties = null; + private XMPPError error = null; + + /** + * Returns the unique ID of the packet. The returned value could be <tt>null</tt> when + * ID_NOT_AVAILABLE was set as the packet's id. + * + * @return the packet's unique ID or <tt>null</tt> if the packet's id is not available. + */ + public String getPacketID() { + if (ID_NOT_AVAILABLE.equals(packetID)) { + return null; + } + + if (packetID == null) { + packetID = nextID(); + } + return packetID; + } + + /** + * Sets the unique ID of the packet. To indicate that a packet has no id + * pass the constant ID_NOT_AVAILABLE as the packet's id value. + * + * @param packetID the unique ID for the packet. + */ + public void setPacketID(String packetID) { + this.packetID = packetID; + } + + /** + * Returns who the packet is being sent "to", or <tt>null</tt> if + * the value is not set. The XMPP protocol often makes the "to" + * attribute optional, so it does not always need to be set. + * + * @return who the packet is being sent to, or <tt>null</tt> if the + * value has not been set. + */ + public String getTo() { + return to; + } + + /** + * Sets who the packet is being sent "to". The XMPP protocol often makes + * the "to" attribute optional, so it does not always need to be set. + * + * @param to who the packet is being sent to. + */ + public void setTo(String to) { + this.to = to; + } + + /** + * Returns who the packet is being sent "from" or <tt>null</tt> if + * the value is not set. The XMPP protocol often makes the "from" + * attribute optional, so it does not always need to be set. + * + * @return who the packet is being sent from, or <tt>null</tt> if the + * valud has not been set. + */ + public String getFrom() { + return from; + } + + /** + * Sets who the packet is being sent "from". The XMPP protocol often + * makes the "from" attribute optional, so it does not always need to + * be set. + * + * @param from who the packet is being sent to. + */ + public void setFrom(String from) { + this.from = from; + } + + /** + * Returns the error associated with this packet, or <tt>null</tt> if there are + * no errors. + * + * @return the error sub-packet or <tt>null</tt> if there isn't an error. + */ + public XMPPError getError() { + return error; + } + + /** + * Sets the error for this packet. + * + * @param error the error to associate with this packet. + */ + public void setError(XMPPError error) { + this.error = error; + } + + /** + * Returns an Iterator for the packet extensions attached to the packet. + * + * @return an Iterator for the packet extensions. + */ + public synchronized Iterator getExtensions() { + if (packetExtensions == null) { + return Collections.EMPTY_LIST.iterator(); + } + return Collections.unmodifiableList(new ArrayList(packetExtensions)).iterator(); + } + + /** + * Returns the first packet extension that matches the specified element name and + * namespace, or <tt>null</tt> if it doesn't exist. Packet extensions are + * are arbitrary XML sub-documents in standard XMPP packets. By default, a + * DefaultPacketExtension instance will be returned for each extension. However, + * PacketExtensionProvider instances can be registered with the + * {@link org.jivesoftware.smack.provider.ProviderManager ProviderManager} + * class to handle custom parsing. In that case, the type of the Object + * will be determined by the provider. + * + * @param elementName the XML element name of the packet extension. + * @param namespace the XML element namespace of the packet extension. + * @return the extension, or <tt>null</tt> if it doesn't exist. + */ + public synchronized PacketExtension getExtension(String elementName, String namespace) { + if (packetExtensions == null || elementName == null || namespace == null) { + return null; + } + for (Iterator i=packetExtensions.iterator(); i.hasNext(); ) { + PacketExtension ext = (PacketExtension)i.next(); + if (elementName.equals(ext.getElementName()) && namespace.equals(ext.getNamespace())) { + return ext; + } + } + return null; + } + + /** + * Adds a packet extension to the packet. + * + * @param extension a packet extension. + */ + public synchronized void addExtension(PacketExtension extension) { + if (packetExtensions == null) { + packetExtensions = new ArrayList(); + } + packetExtensions.add(extension); + } + + /** + * Removes a packet extension from the packet. + * + * @param extension the packet extension to remove. + */ + public synchronized void removeExtension(PacketExtension extension) { + if (packetExtensions != null) { + packetExtensions.remove(extension); + } + } + + /** + * Returns the packet property with the specified name or <tt>null</tt> if the + * property doesn't exist. Property values that were orginally primitives will + * be returned as their object equivalent. For example, an int property will be + * returned as an Integer, a double as a Double, etc. + * + * @param name the name of the property. + * @return the property, or <tt>null</tt> if the property doesn't exist. + */ + public synchronized Object getProperty(String name) { + if (properties == null) { + return null; + } + return properties.get(name); + } + + /** + * Sets a packet property with an int value. + * + * @param name the name of the property. + * @param value the value of the property. + */ + public void setProperty(String name, int value) { + setProperty(name, new Integer(value)); + } + + /** + * Sets a packet property with a long value. + * + * @param name the name of the property. + * @param value the value of the property. + */ + public void setProperty(String name, long value) { + setProperty(name, new Long(value)); + } + + /** + * Sets a packet property with a float value. + * + * @param name the name of the property. + * @param value the value of the property. + */ + public void setProperty(String name, float value) { + setProperty(name, new Float(value)); + } + + /** + * Sets a packet property with a double value. + * + * @param name the name of the property. + * @param value the value of the property. + */ + public void setProperty(String name, double value) { + setProperty(name, new Double(value)); + } + + /** + * Sets a packet property with a bboolean value. + * + * @param name the name of the property. + * @param value the value of the property. + */ + public void setProperty(String name, boolean value) { + setProperty(name, new Boolean(value)); + } + + /** + * Sets a property with an Object as the value. The value must be Serializable + * or an IllegalArgumentException will be thrown. + * + * @param name the name of the property. + * @param value the value of the property. + */ + public synchronized void setProperty(String name, Object value) { + if (!(value instanceof Serializable)) { + throw new IllegalArgumentException("Value must be serialiazble"); + } + if (properties == null) { + properties = new HashMap(); + } + properties.put(name, value); + } + + /** + * Deletes a property. + * + * @param name the name of the property to delete. + */ + public synchronized void deleteProperty(String name) { + if (properties == null) { + return; + } + properties.remove(name); + } + + /** + * Returns an Iterator for all the property names that are set. + * + * @return an Iterator for all property names. + */ + public synchronized Iterator getPropertyNames() { + if (properties == null) { + return Collections.EMPTY_LIST.iterator(); + } + return properties.keySet().iterator(); + } + + /** + * Returns the packet as XML. Every concrete extension of Packet must implement + * this method. In addition to writing out packet-specific data, every sub-class + * should also write out the error and the extensions data if they are defined. + * + * @return the XML format of the packet as a String. + */ + public abstract String toXML(); + + /** + * Returns the extension sub-packets (including properties data) as an XML + * String, or the Empty String if there are no packet extensions. + * + * @return the extension sub-packets as XML or the Empty String if there + * are no packet extensions. + */ + protected synchronized String getExtensionsXML() { + StringBuffer buf = new StringBuffer(); + // Add in all standard extension sub-packets. + Iterator extensions = getExtensions(); + while (extensions.hasNext()) { + PacketExtension extension = (PacketExtension)extensions.next(); + buf.append(extension.toXML()); + } + // Add in packet properties. + if (properties != null && !properties.isEmpty()) { + buf.append("<properties xmlns=\"http://www.jivesoftware.com/xmlns/xmpp/properties\">"); + // Loop through all properties and write them out. + for (Iterator i=getPropertyNames(); i.hasNext(); ) { + String name = (String)i.next(); + Object value = getProperty(name); + buf.append("<property>"); + buf.append("<name>").append(StringUtils.escapeForXML(name)).append("</name>"); + buf.append("<value type=\""); + if (value instanceof Integer) { + buf.append("integer\">").append(value).append("</value>"); + } + else if (value instanceof Long) { + buf.append("long\">").append(value).append("</value>"); + } + else if (value instanceof Float) { + buf.append("float\">").append(value).append("</value>"); + } + else if (value instanceof Double) { + buf.append("double\">").append(value).append("</value>"); + } + else if (value instanceof Boolean) { + buf.append("boolean\">").append(value).append("</value>"); + } + else if (value instanceof String) { + buf.append("string\">"); + buf.append(StringUtils.escapeForXML((String)value)); + buf.append("</value>"); + } + // Otherwise, it's a generic Serializable object. Serialized objects are in + // a binary format, which won't work well inside of XML. Therefore, we base-64 + // encode the binary data before adding it. + else { + ByteArrayOutputStream byteStream = null; + ObjectOutputStream out = null; + try { + byteStream = new ByteArrayOutputStream(); + out = new ObjectOutputStream(byteStream); + out.writeObject(value); + buf.append("java-object\">"); + String encodedVal = StringUtils.encodeBase64(byteStream.toByteArray()); + buf.append(encodedVal).append("</value>"); + } + catch (Exception e) { + e.printStackTrace(); + } + finally { + if (out != null) { + try { out.close(); } catch (Exception e) { } + } + if (byteStream != null) { + try { byteStream.close(); } catch (Exception e) { } + } + } + } + buf.append("</property>"); + } + buf.append("</properties>"); + } + return buf.toString(); + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/PacketExtension.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/PacketExtension.java new file mode 100644 index 000000000..e402e9d29 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/PacketExtension.java @@ -0,0 +1,56 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.packet; + +/** + * Interface to represent packet extensions. A packet extension is an XML subdocument + * with a root element name and namespace. Packet extensions are used to provide + * extended functionality beyond what is in the base XMPP specification. Examples of + * packet extensions include message events, message properties, and extra presence data. + * IQ packets cannot contain packet extensions. + * + * @see DefaultPacketExtension + * @see org.jivesoftware.smack.provider.PacketExtensionProvider + * @author Matt Tucker + */ +public interface PacketExtension { + + /** + * Returns the root element name. + * + * @return the element name. + */ + public String getElementName(); + + /** + * Returns the root element XML namespace. + * + * @return the namespace. + */ + public String getNamespace(); + + /** + * Returns the XML reppresentation of the PacketExtension. + * + * @return the packet extension as XML. + */ + public String toXML(); +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Presence.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Presence.java new file mode 100644 index 000000000..52e17d0f0 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Presence.java @@ -0,0 +1,327 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.packet; + +import org.jivesoftware.smack.util.StringUtils; + +/** + * Represents XMPP presence packets. Every presence packet has a type, which is one of + * the following values: + * <ul> + * <li><tt>Presence.Type.AVAILABLE</tt> -- (Default) indicates the user is available to + * receive messages. + * <li><tt>Presence.Type.UNAVAILABLE</tt> -- the user is unavailable to receive messages. + * <li><tt>Presence.Type.SUBSCRIBE</tt> -- request subscription to recipient's presence. + * <li><tt>Presence.Type.SUBSCRIBED</tt> -- grant subscription to sender's presence. + * <li><tt>Presence.Type.UNSUBSCRIBE</tt> -- request removal of subscription to sender's + * presence. + * <li><tt>Presence.Type.UNSUBSCRIBED</tt> -- grant removal of subscription to sender's + * presence. + * <li><tt>Presence.Type.ERROR</tt> -- the presence packet contains an error message. + * </ul><p> + * + * A number of attributes are optional: + * <ul> + * <li>Status -- free-form text describing a user's presence (i.e., gone to lunch). + * <li>Priority -- non-negative numerical priority of a sender's resource. The + * highest resource priority is the default recipient of packets not addressed + * to a particular resource. + * <li>Mode -- one of five presence modes: available (the default), chat, away, + * xa (extended away, and dnd (do not disturb). + * </ul><p> + * + * Presence packets are used for two purposes. First, to notify the server of our + * the clients current presence status. Second, they are used to subscribe and + * unsubscribe users from the roster. + * + * @see RosterPacket + * @author Matt Tucker + */ +public class Presence extends Packet { + + private Type type = Type.AVAILABLE; + private String status = null; + private int priority = -1; + private Mode mode = Mode.AVAILABLE; + + /** + * Creates a new presence update. Status, priority, and mode are left un-set. + * + * @param type the type. + */ + public Presence(Type type) { + this.type = type; + } + + /** + * Creates a new presence update with a specified status, priority, and mode. + * + * @param type the type. + * @param status a text message describing the presence update. + * @param priority the priority of this presence update. + * @param mode the mode type for this presence update. + */ + public Presence(Type type, String status, int priority, Mode mode) { + this.type = type; + this.status = status; + this.priority = priority; + this.mode = mode; + } + + /** + * Returns the type of this presence packet. + * + * @return the type of the presence packet. + */ + public Type getType() { + return type; + } + + /** + * Sets the type of the presence packet. + * + * @param type the type of the presence packet. + */ + public void setType(Type type) { + this.type = type; + } + + /** + * Returns the status message of the presence update, or <tt>null</tt> if there + * is not a status. The status is free-form text describing a user's presence + * (i.e., "gone to lunch"). + * + * @return the status message. + */ + public String getStatus() { + return status; + } + + /** + * Sets the status message of the presence update. The status is free-form text + * describing a user's presence (i.e., "gone to lunch"). + * + * @param status the status message. + */ + public void setStatus(String status) { + this.status = status; + } + + /** + * Returns the priority of the presence, or -1 if no priority has been set. + * + * @return the priority. + */ + public int getPriority() { + return priority; + } + + /** + * Sets the priority of the presence. The valid range is -128 through 128. + * + * @param priority the priority of the presence. + * @throws IllegalArgumentException if the priority is outside the valid range. + */ + public void setPriority(int priority) { + if (priority < -128 || priority > 128) { + throw new IllegalArgumentException("Priority value " + priority + + " is not valid. Valid range is -128 through 128."); + } + this.priority = priority; + } + + /** + * Returns the mode of the presence update. + * + * @return the mode. + */ + public Mode getMode() { + return mode; + } + + /** + * Sets the mode of the presence update. For the standard "available" state, set + * the mode to <tt>null</tt>. + * + * @param mode the mode. + */ + public void setMode(Mode mode) { + this.mode = mode; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<presence"); + if (getPacketID() != null) { + buf.append(" id=\"").append(getPacketID()).append("\""); + } + if (getTo() != null) { + buf.append(" to=\"").append(StringUtils.escapeForXML(getTo())).append("\""); + } + if (getFrom() != null) { + buf.append(" from=\"").append(StringUtils.escapeForXML(getFrom())).append("\""); + } + if (type != Type.AVAILABLE) { + buf.append(" type=\"").append(type).append("\""); + } + buf.append(">"); + if (status != null) { + buf.append("<status>").append(status).append("</status>"); + } + if (priority != -1) { + buf.append("<priority>").append(priority).append("</priority>"); + } + if (mode != null && mode != Mode.AVAILABLE) { + buf.append("<show>").append(mode).append("</show>"); + } + + buf.append(this.getExtensionsXML()); + + // Add the error sub-packet, if there is one. + XMPPError error = getError(); + if (error != null) { + buf.append(error.toXML()); + } + + buf.append("</presence>"); + + return buf.toString(); + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(type); + if (mode != null) { + buf.append(": ").append(mode); + } + if (status != null) { + buf.append(" (").append(status).append(")"); + } + return buf.toString(); + } + + /** + * A typsafe enum class to represent the presecence type. + */ + public static class Type { + + public static final Type AVAILABLE = new Type("available"); + public static final Type UNAVAILABLE = new Type("unavailable"); + public static final Type SUBSCRIBE = new Type("subscribe"); + public static final Type SUBSCRIBED = new Type("subscribed"); + public static final Type UNSUBSCRIBE = new Type("unsubscribe"); + public static final Type UNSUBSCRIBED = new Type("unsubscribed"); + public static final Type ERROR = new Type("error"); + + private String value; + + private Type(String value) { + this.value = value; + } + + public String toString() { + return value; + } + + /** + * Returns the type constant associated with the String value. + */ + public static Type fromString(String value) { + if (value == null) { + return AVAILABLE; + } + value = value.toLowerCase(); + if ("unavailable".equals(value)) { + return UNAVAILABLE; + } + else if ("subscribe".equals(value)) { + return SUBSCRIBE; + } + else if ("subscribed".equals(value)) { + return SUBSCRIBED; + } + else if ("unsubscribe".equals(value)) { + return UNSUBSCRIBE; + } + else if ("unsubscribed".equals(value)) { + return UNSUBSCRIBED; + } + else if ("error".equals(value)) { + return ERROR; + } + // Default to available. + else { + return AVAILABLE; + } + } + } + + /** + * A typsafe enum class to represent the presence mode. + */ + public static class Mode { + + public static final Mode AVAILABLE = new Mode("available"); + public static final Mode CHAT = new Mode("chat"); + public static final Mode AWAY = new Mode("away"); + public static final Mode EXTENDED_AWAY = new Mode("xa"); + public static final Mode DO_NOT_DISTURB = new Mode("dnd"); + public static final Mode INVISIBLE = new Mode("invisible"); + + private String value; + + private Mode(String value) { + this.value = value; + } + + public String toString() { + return value; + } + + /** + * Returns the mode constant associated with the String value. + */ + public static Mode fromString(String value) { + if (value == null) { + return AVAILABLE; + } + value = value.toLowerCase(); + if (value.equals("chat")) { + return CHAT; + } + else if (value.equals("away")) { + return AWAY; + } + else if (value.equals("xa")) { + return EXTENDED_AWAY; + } + else if (value.equals("dnd")) { + return DO_NOT_DISTURB; + } + else if (value.equals("invisible")) { + return INVISIBLE; + } + else { + return AVAILABLE; + } + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Registration.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Registration.java new file mode 100644 index 000000000..07b06daa7 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Registration.java @@ -0,0 +1,113 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.packet; + +import java.util.Map; +import java.util.Iterator; + +/** + * Represents registration packets. An empty GET query will cause the server to return information + * about it's registration support. SET queries can be used to create accounts or update + * existing account information. XMPP servers may require a number of attributes to be set + * when creating a new account. The standard account attributes are as follows: + * <ul> + * <li>name -- the user's name. + * <li>first -- the user's first name. + * <li>last -- the user's last name. + * <li>email -- the user's email address. + * <li>city -- the user's city. + * <li>state -- the user's state. + * <li>zip -- the user's ZIP code. + * <li>phone -- the user's phone number. + * <li>url -- the user's website. + * <li>date -- the date the registration took place. + * <li>misc -- other miscellaneous information to associate with the account. + * <li>text -- textual information to associate with the account. + * <li>remove -- empty flag to remove account. + * </ul> + * + * @author Matt Tucker + */ +public class Registration extends IQ { + + private String instructions = null; + private Map attributes = null; + + /** + * Returns the registration instructions, or <tt>null</tt> if no instructions + * have been set. If present, instructions should be displayed to the end-user + * that will complete the registration process. + * + * @return the registration instructions, or <tt>null</tt> if there are none. + */ + public String getInstructions() { + return instructions; + } + + /** + * Sets the registration instructions. + * + * @param instructions the registration instructions. + */ + public void setInstructions(String instructions) { + this.instructions = instructions; + } + + /** + * Returns the map of String key/value pairs of account attributes. + * + * @return the account attributes. + */ + public Map getAttributes() { + return attributes; + } + + /** + * Sets the account attributes. The map must only contain String key/value pairs. + * + * @param attributes the account attributes. + */ + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"jabber:iq:register\">"); + if (instructions != null) { + buf.append("<instructions>").append(instructions).append("</instructions>"); + } + if (attributes != null && attributes.size() > 0) { + Iterator fieldNames = attributes.keySet().iterator(); + while (fieldNames.hasNext()) { + String name = (String)fieldNames.next(); + String value = (String)attributes.get(name); + buf.append("<").append(name).append(">"); + buf.append(value); + buf.append("</").append(name).append(">"); + } + } + // Add packet extensions, if any are defined. + buf.append(getExtensionsXML()); + buf.append("</query>"); + return buf.toString(); + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/RosterPacket.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/RosterPacket.java new file mode 100644 index 000000000..30048b7c3 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/RosterPacket.java @@ -0,0 +1,348 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.packet; + +import java.util.*; + +/** + * Represents XMPP roster packets. + * + * @author Matt Tucker + */ +public class RosterPacket extends IQ { + + private List rosterItems = new ArrayList(); + + /** + * Adds a roster item to the packet. + * + * @param item a roster item. + */ + public void addRosterItem(Item item) { + synchronized (rosterItems) { + rosterItems.add(item); + } + } + + /** + * Returns the number of roster items in this roster packet. + * + * @return the number of roster items. + */ + public int getRosterItemCount() { + synchronized (rosterItems) { + return rosterItems.size(); + } + } + + /** + * Returns an Iterator for the roster items in the packet. + * + * @return and Iterator for the roster items in the packet. + */ + public Iterator getRosterItems() { + synchronized (rosterItems) { + List entries = Collections.unmodifiableList(new ArrayList(rosterItems)); + return entries.iterator(); + } + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"jabber:iq:roster\">"); + synchronized (rosterItems) { + for (int i=0; i<rosterItems.size(); i++) { + Item entry = (Item)rosterItems.get(i); + buf.append(entry.toXML()); + } + } + buf.append("</query>"); + return buf.toString(); + } + + /** + * A roster item, which consists of a JID, their name, the type of subscription, and + * the groups the roster item belongs to. + */ + public static class Item { + + private String user; + private String name; + private ItemType itemType; + private ItemStatus itemStatus; + private List groupNames; + + /** + * Creates a new roster item. + * + * @param user the user. + * @param name the user's name. + */ + public Item(String user, String name) { + this.user = user.toLowerCase(); + this.name = name; + itemType = null; + itemStatus = null; + groupNames = new ArrayList(); + } + + /** + * Returns the user. + * + * @return the user. + */ + public String getUser() { + return user; + } + + /** + * Returns the user's name. + * + * @return the user's name. + */ + public String getName() { + return name; + } + + /** + * Sets the user's name. + * + * @param name the user's name. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the roster item type. + * + * @return the roster item type. + */ + public ItemType getItemType() { + return itemType; + } + + /** + * Sets the roster item type. + * + * @param itemType the roster item type. + */ + public void setItemType(ItemType itemType) { + this.itemType = itemType; + } + + /** + * Returns the roster item status. + * + * @return the roster item status. + */ + public ItemStatus getItemStatus() { + return itemStatus; + } + + /** + * Sets the roster item status. + * + * @param itemStatus the roster item status. + */ + public void setItemStatus(ItemStatus itemStatus) { + this.itemStatus = itemStatus; + } + + /** + * Returns an Iterator for the group names (as Strings) that the roster item + * belongs to. + * + * @return an Iterator for the group names. + */ + public Iterator getGroupNames() { + synchronized (groupNames) { + return Collections.unmodifiableList(groupNames).iterator(); + } + } + + /** + * Adds a group name. + * + * @param groupName the group name. + */ + public void addGroupName(String groupName) { + synchronized (groupNames) { + if (!groupNames.contains(groupName)) { + groupNames.add(groupName); + } + } + } + + /** + * Removes a group name. + * + * @param groupName the group name. + */ + public void removeGroupName(String groupName) { + synchronized (groupNames) { + groupNames.remove(groupName); + } + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<item jid=\"").append(user).append("\""); + if (name != null) { + buf.append(" name=\"").append(name).append("\""); + } + if (itemType != null) { + buf.append(" subscription=\"").append(itemType).append("\""); + } + if (itemStatus != null) { + buf.append(" ask=\"").append(itemStatus).append("\""); + } + buf.append(">"); + synchronized (groupNames) { + for (int i=0; i<groupNames.size(); i++) { + String groupName = (String)groupNames.get(i); + buf.append("<group>").append(groupName).append("</group>"); + } + } + buf.append("</item>"); + return buf.toString(); + } + } + + /** + * The subscription status of a roster item. An optional element that indicates + * the subscription status if a change request is pending. + */ + public static class ItemStatus { + + /** + * Request to subcribe. + */ + public static final ItemStatus SUBSCRIPTION_PENDING = new ItemStatus("subscribe"); + + /** + * Request to unsubscribe. + */ + public static final ItemStatus UNSUBCRIPTION_PENDING = new ItemStatus("unsubscribe"); + + public static ItemStatus fromString(String value) { + if (value == null) { + return null; + } + value = value.toLowerCase(); + if ("unsubscribe".equals(value)) { + return SUBSCRIPTION_PENDING; + } + else if ("subscribe".equals(value)) { + return SUBSCRIPTION_PENDING; + } + else { + return null; + } + } + + private String value; + + /** + * Returns the item status associated with the specified string. + * + * @param value the item status. + */ + private ItemStatus(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } + + /** + * The subscription type of a roster item. + */ + public static class ItemType { + + /** + * The user and subscriber have no interest in each other's presence. + */ + public static final ItemType NONE = new ItemType("none"); + + /** + * The user is interested in receiving presence updates from the subscriber. + */ + public static final ItemType TO = new ItemType("to"); + + /** + * The subscriber is interested in receiving presence updates from the user. + */ + public static final ItemType FROM = new ItemType("from"); + + /** + * The user and subscriber have a mutual interest in each other's presence. + */ + public static final ItemType BOTH = new ItemType("both"); + + /** + * The user wishes to stop receiving presence updates from the subscriber. + */ + public static final ItemType REMOVE = new ItemType("remove"); + + public static ItemType fromString(String value) { + if (value == null) { + return null; + } + value = value.toLowerCase(); + if ("none".equals(value)) { + return NONE; + } + else if ("to".equals(value)) { + return TO; + } + else if ("from".equals(value)) { + return FROM; + } + else if ("both".equals(value)) { + return BOTH; + } + else if ("remove".equals(value)) { + return REMOVE; + } + else { + return null; + } + } + + private String value; + + /** + * Returns the item type associated with the specified string. + * + * @param value the item type. + */ + public ItemType(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Session.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Session.java new file mode 100644 index 000000000..18cfd88cc --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/Session.java @@ -0,0 +1,45 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.packet; + +/** + * IQ packet that will be sent to the server to establish a session.<p> + * + * If a server supports sessions, it MUST include a <i>session</i> element in the + * stream features it advertises to a client after the completion of stream authentication. + * Upon being informed that session establishment is required by the server the client MUST + * establish a session if it desires to engage in instant messaging and presence functionality.<p> + * + * For more information refer to the following + * <a href=http://www.xmpp.org/specs/rfc3921.html#session>link</a>. + * + * @author Gaston Dombiak + */ +public class Session extends IQ { + + public Session() { + setType(IQ.Type.SET); + } + + public String getChildElementXML() { + return "<session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"/>"; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/StreamError.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/StreamError.java new file mode 100644 index 000000000..f6edb5450 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/StreamError.java @@ -0,0 +1,106 @@ +/** + * $Revision$ + * $Date$ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.packet; + +/** + * Represents a stream error packet. Stream errors are unrecoverable errors where the server + * will close the unrelying TCP connection after the stream error was sent to the client. + * These is the list of stream errors as defined in the XMPP spec:<p> + * + * <table border=1> + * <tr><td><b>Code</b></td><td><b>Description</b></td></tr> + * <tr><td> bad-format </td><td> the entity has sent XML that cannot be processed </td></tr> + * <tr><td> unsupported-encoding </td><td> the entity has sent a namespace prefix that is + * unsupported </td></tr> + * <tr><td> bad-namespace-prefix </td><td> Remote Server Timeout </td></tr> + * <tr><td> conflict </td><td> the server is closing the active stream for this entity + * because a new stream has been initiated that conflicts with the existing + * stream. </td></tr> + * <tr><td> connection-timeout </td><td> the entity has not generated any traffic over + * the stream for some period of time. </td></tr> + * <tr><td> host-gone </td><td> the value of the 'to' attribute provided by the initiating + * entity in the stream header corresponds to a hostname that is no longer hosted by + * the server. </td></tr> + * <tr><td> host-unknown </td><td> the value of the 'to' attribute provided by the + * initiating entity in the stream header does not correspond to a hostname that is + * hosted by the server. </td></tr> + * <tr><td> improper-addressing </td><td> a stanza sent between two servers lacks a 'to' + * or 'from' attribute </td></tr> + * <tr><td> internal-server-error </td><td> the server has experienced a + * misconfiguration. </td></tr> + * <tr><td> invalid-from </td><td> the JID or hostname provided in a 'from' address does + * not match an authorized JID. </td></tr> + * <tr><td> invalid-id </td><td> the stream ID or dialback ID is invalid or does not match + * an ID previously provided. </td></tr> + * <tr><td> invalid-namespace </td><td> the streams namespace name is invalid. </td></tr> + * <tr><td> invalid-xml </td><td> the entity has sent invalid XML over the stream. </td></tr> + * <tr><td> not-authorized </td><td> the entity has attempted to send data before the + * stream has been authenticated </td></tr> + * <tr><td> policy-violation </td><td> the entity has violated some local service + * policy. </td></tr> + * <tr><td> remote-connection-failed </td><td> Rthe server is unable to properly connect + * to a remote entity. </td></tr> + * <tr><td> resource-constraint </td><td> Rthe server lacks the system resources necessary + * to service the stream. </td></tr> + * <tr><td> restricted-xml </td><td> the entity has attempted to send restricted XML + * features. </td></tr> + * <tr><td> see-other-host </td><td> the server will not provide service to the initiating + * entity but is redirecting traffic to another host. </td></tr> + * <tr><td> system-shutdown </td><td> the server is being shut down and all active streams + * are being closed. </td></tr> + * <tr><td> undefined-condition </td><td> the error condition is not one of those defined + * by the other conditions in this list. </td></tr> + * <tr><td> unsupported-encoding </td><td> the initiating entity has encoded the stream in + * an encoding that is not supported. </td></tr> + * <tr><td> unsupported-stanza-type </td><td> the initiating entity has sent a first-level + * child of the stream that is not supported. </td></tr> + * <tr><td> unsupported-version </td><td> the value of the 'version' attribute provided by + * the initiating entity in the stream header specifies a version of XMPP that is not + * supported. </td></tr> + * <tr><td> xml-not-well-formed </td><td> the initiating entity has sent XML that is + * not well-formed. </td></tr> + * </table> + * + * @author Gaston Dombiak + */ +public class StreamError { + + private String code; + + public StreamError(String code) { + super(); + this.code = code; + } + + /** + * Returns the error code. + * + * @return the error code. + */ + public String getCode() { + return code; + } + + public String toString() { + StringBuffer txt = new StringBuffer(); + txt.append("stream:error (").append(code).append(")"); + return txt.toString(); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/XMPPError.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/XMPPError.java new file mode 100644 index 000000000..6d90c48e6 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/XMPPError.java @@ -0,0 +1,117 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.packet; + +/** + * Represents a XMPP error sub-packet. Typically, a server responds to a request that has + * problems by sending the packet back and including an error packet. Each error has a code + * as well as as an optional text explanation. Typical error codes are as follows:<p> + * + * <table border=1> + * <tr><td><b>Code</b></td><td><b>Description</b></td></tr> + * <tr><td> 302 </td><td> Redirect </td></tr> + * <tr><td> 400 </td><td> Bad Request </td></tr> + * <tr><td> 401 </td><td> Unauthorized </td></tr> + * <tr><td> 402 </td><td> Payment Required </td></tr> + * <tr><td> 403 </td><td> Forbidden </td></tr> + * <tr><td> 404 </td><td> Not Found </td></tr> + * <tr><td> 405 </td><td> Not Allowed </td></tr> + * <tr><td> 406 </td><td> Not Acceptable </td></tr> + * <tr><td> 407 </td><td> Registration Required </td></tr> + * <tr><td> 408 </td><td> Request Timeout </td></tr> + * <tr><td> 409 </td><td> Conflict </td></tr> + * <tr><td> 500 </td><td> Internal Server XMPPError </td></tr> + * <tr><td> 501 </td><td> Not Implemented </td></tr> + * <tr><td> 502 </td><td> Remote Server Error </td></tr> + * <tr><td> 503 </td><td> Service Unavailable </td></tr> + * <tr><td> 504 </td><td> Remote Server Timeout </td></tr> + * </table> + * + * @author Matt Tucker + */ +public class XMPPError { + + private int code; + private String message; + + /** + * Creates a new error with the specified code and no message.. + * + * @param code the error code. + */ + public XMPPError(int code) { + this.code = code; + this.message = null; + } + + /** + * Creates a new error with the specified code and message. + * + * @param code the error code. + * @param message a message describing the error. + */ + public XMPPError(int code, String message) { + this.code = code; + this.message = message; + } + + /** + * Returns the error code. + * + * @return the error code. + */ + public int getCode() { + return code; + } + + /** + * Returns the message describing the error, or null if there is no message. + * + * @return the message describing the error, or null if there is no message. + */ + public String getMessage() { + return message; + } + + /** + * Returns the error as XML. + * + * @return the error as XML. + */ + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<error code=\"").append(code).append("\">"); + if (message != null) { + buf.append(message); + } + buf.append("</error>"); + return buf.toString(); + } + + public String toString() { + StringBuffer txt = new StringBuffer(); + txt.append("(").append(code).append(")"); + if (message != null) { + txt.append(" ").append(message); + } + return txt.toString(); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/package.html new file mode 100644 index 000000000..18a6405c8 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/packet/package.html @@ -0,0 +1 @@ +<body>XML packets that are part of the XMPP protocol.</body>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/IQProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/IQProvider.java new file mode 100644 index 000000000..8273283f1 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/IQProvider.java @@ -0,0 +1,47 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.xmlpull.v1.XmlPullParser; + +/** + * An interface for parsing custom IQ packets. Each IQProvider must be registered with + * the ProviderManager class for it to be used. Every implementation of this + * interface <b>must</b> have a public, no-argument constructor. + * + * @author Matt Tucker + */ +public interface IQProvider { + + /** + * Parse the IQ sub-document and create an IQ instance. Each IQ must have a + * single child element. At the beginning of the method call, the xml parser + * will be positioned at the opening tag of the IQ child element. At the end + * of the method call, the parser <b>must</b> be positioned on the closing tag + * of the child element. + * + * @param parser an XML parser. + * @return a new IQ instance. + * @throws Exception if an error occurs parsing the XML. + */ + public IQ parseIQ(XmlPullParser parser) throws Exception; +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/PacketExtensionProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/PacketExtensionProvider.java new file mode 100644 index 000000000..40baeaafc --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/PacketExtensionProvider.java @@ -0,0 +1,46 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.provider; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.xmlpull.v1.XmlPullParser; + +/** + * An interface for parsing custom packets extensions. Each PacketExtensionProvider must + * be registered with the ProviderManager class for it to be used. Every implementation + * of this interface <b>must</b> have a public, no-argument constructor. + * + * @author Matt Tucker + */ +public interface PacketExtensionProvider { + + /** + * Parse an extension sub-packet and create a PacketExtension instance. At + * the beginning of the method call, the xml parser will be positioned on the + * opening element of the packet extension. At the end of the method call, the + * parser <b>must</b> be positioned on the closing element of the packet extension. + * + * @param parser an XML parser. + * @return a new IQ instance. + * @throws java.lang.Exception if an error occurs parsing the XML. + */ + public PacketExtension parseExtension(XmlPullParser parser) throws Exception; +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/ProviderManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/ProviderManager.java new file mode 100644 index 000000000..34a6c356f --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/ProviderManager.java @@ -0,0 +1,396 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.PacketExtension; +import org.xmlpull.v1.*; + +import java.util.*; +import java.net.URL; + +/** + * Manages providers for parsing custom XML sub-documents of XMPP packets. Two types of + * providers exist:<ul> + * <li>IQProvider -- parses IQ requests into Java objects. + * <li>PacketExtension -- parses XML sub-documents attached to packets into + * PacketExtension instances.</ul> + * + * <b>IQProvider</b><p> + * + * By default, Smack only knows how to process IQ packets with sub-packets that + * are in a few namespaces such as:<ul> + * <li>jabber:iq:auth + * <li>jabber:iq:roster + * <li>jabber:iq:register</ul> + * + * Because many more IQ types are part of XMPP and its extensions, a pluggable IQ parsing + * mechanism is provided. IQ providers are registered programatically or by creating a + * smack.providers file in the META-INF directory of your JAR file. The file is an XML + * document that contains one or more iqProvider entries, as in the following example: + * + * <pre> + * <?xml version="1.0"?> + * <smackProviders> + * <iqProvider> + * <elementName>query</elementName> + * <namespace>jabber:iq:time</namespace> + * <className>org.jivesoftware.smack.packet.Time</className> + * </iqProvider> + * </smackProviders></pre> + * + * Each IQ provider is associated with an element name and a namespace. If multiple provider + * entries attempt to register to handle the same namespace, the first entry loaded from the + * classpath will take precedence. The IQ provider class can either implement the IQProvider + * interface, or extend the IQ class. In the former case, each IQProvider is responsible for + * parsing the raw XML stream to create an IQ instance. In the latter case, bean introspection + * is used to try to automatically set properties of the IQ instance using the values found + * in the IQ packet XML. For example, an XMPP time packet resembles the following: + * <pre> + * <iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'> + * <query xmlns='jabber:iq:time'> + * <utc>20020910T17:58:35</utc> + * <tz>MDT</tz> + * <display>Tue Sep 10 12:58:35 2002</display> + * </query> + * </iq></pre> + * + * In order for this packet to be automatically mapped to the Time object listed in the + * providers file above, it must have the methods setUtc(String), setTz(String), and + * setDisplay(String). The introspection service will automatically try to convert the String + * value from the XML into a boolean, int, long, float, double, or Class depending on the + * type the IQ instance expects.<p> + * + * A pluggable system for packet extensions, child elements in a custom namespace for + * message and presence packets, also exists. Each extension provider + * is registered with a name space in the smack.providers file as in the following example: + * + * <pre> + * <?xml version="1.0"?> + * <smackProviders> + * <extensionProvider> + * <elementName>x</elementName> + * <namespace>jabber:iq:event</namespace> + * <className>org.jivesoftware.smack.packet.MessageEvent</className> + * </extensionProvider> + * </smackProviders></pre> + * + * If multiple provider entries attempt to register to handle the same element name and namespace, + * the first entry loaded from the classpath will take precedence. Whenever a packet extension + * is found in a packet, parsing will be passed to the correct provider. Each provider + * can either implement the PacketExtensionProvider interface or be a standard Java Bean. In + * the former case, each extension provider is responsible for parsing the raw XML stream to + * contruct an object. In the latter case, bean introspection is used to try to automatically + * set the properties of the class using the values in the packet extension sub-element. When an + * extension provider is not registered for an element name and namespace combination, Smack will + * store all top-level elements of the sub-packet in DefaultPacketExtension object and then + * attach it to the packet. + * + * @author Matt Tucker + */ +public class ProviderManager { + + private static ProviderManager defaultProvider = null; + + private Map extensionProviders = new Hashtable(); + private Map iqProviders = new Hashtable(); + + public static ProviderManager getDefault() { + if (defaultProvider == null) + defaultProvider = new ProviderManager(); + return defaultProvider; + } + + public static void setDefault(ProviderManager value) { + if (defaultProvider != null) + throw new IllegalStateException("ProviderManager default already set"); + defaultProvider = value; + } + + public ProviderManager() { + super(); + initialize(); + } + + protected void initialize() { + // Load IQ processing providers. + try { + // Get an array of class loaders to try loading the providers files from. + ClassLoader[] classLoaders = getClassLoaders(); + for (int i=0; i<classLoaders.length; i++) { + Enumeration providerEnum = classLoaders[i].getResources( + "META-INF/smack.providers"); + while (providerEnum.hasMoreElements()) { + URL url = (URL)providerEnum.nextElement(); + java.io.InputStream providerStream = null; + try { + providerStream = url.openStream(); + XmlPullParserFactory factory = XmlPullParserFactory.newInstance( + "org.xmlpull.mxp1.MXParserFactory", null); + factory.setNamespaceAware(true); + XmlPullParser parser = factory.newPullParser(); + parser.setInput(providerStream, "UTF-8"); + int eventType = parser.getEventType(); + do { + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("iqProvider")) { + parser.next(); + parser.next(); + String elementName = parser.nextText(); + parser.next(); + parser.next(); + String namespace = parser.nextText(); + parser.next(); + parser.next(); + String className = parser.nextText(); + // Only add the provider for the namespace if one isn't + // already registered. + String key = getProviderKey(elementName, namespace); + if (!iqProviders.containsKey(key)) { + // Attempt to load the provider class and then create + // a new instance if it's an IQProvider. Otherwise, if it's + // an IQ class, add the class object itself, then we'll use + // reflection later to create instances of the class. + try { + // Add the provider to the map. + Class provider = Class.forName(className); + if (IQProvider.class.isAssignableFrom(provider)) { + iqProviders.put(key, provider.newInstance()); + } + else if (IQ.class.isAssignableFrom(provider)) { + iqProviders.put(key, provider); + } + } + catch (ClassNotFoundException cnfe) { + cnfe.printStackTrace(); + } + } + } + else if (parser.getName().equals("extensionProvider")) { + parser.next(); + parser.next(); + String elementName = parser.nextText(); + parser.next(); + parser.next(); + String namespace = parser.nextText(); + parser.next(); + parser.next(); + String className = parser.nextText(); + // Only add the provider for the namespace if one isn't + // already registered. + String key = getProviderKey(elementName, namespace); + if (!extensionProviders.containsKey(key)) { + // Attempt to load the provider class and then create + // a new instance if it's a Provider. Otherwise, if it's + // a PacketExtension, add the class object itself and + // then we'll use reflection later to create instances + // of the class. + try { + // Add the provider to the map. + Class provider = Class.forName(className); + if (PacketExtensionProvider.class.isAssignableFrom( + provider)) + { + extensionProviders.put(key, provider.newInstance()); + } + else if (PacketExtension.class.isAssignableFrom( + provider)) + { + extensionProviders.put(key, provider); + } + } + catch (ClassNotFoundException cnfe) { + cnfe.printStackTrace(); + } + } + } + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + } + finally { + try { providerStream.close(); } + catch (Exception e) { } + } + } + } + } + catch (Exception e) { } + } + + /** + * Returns the IQ provider registered to the specified XML element name and namespace. + * For example, if a provider was registered to the element name "query" and the + * namespace "jabber:iq:time", then the following packet would trigger the provider: + * + * <pre> + * <iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'> + * <query xmlns='jabber:iq:time'> + * <utc>20020910T17:58:35</utc> + * <tz>MDT</tz> + * <display>Tue Sep 10 12:58:35 2002</display> + * </query> + * </iq></pre> + * + * <p>Note: this method is generally only called by the internal Smack classes. + * + * @param elementName the XML element name. + * @param namespace the XML namespace. + * @return the IQ provider. + */ + public Object getIQProvider(String elementName, String namespace) { + String key = getProviderKey(elementName, namespace); + return iqProviders.get(key); + } + + /** + * Returns an Iterator for all IQProvider instances. + * + * @return an Iterator for all IQProvider instances. + */ + public Iterator getIQProviders() { + return Collections.unmodifiableCollection(new HashMap(iqProviders).values()).iterator(); + } + + /** + * Adds an IQ provider (must be an instance of IQProvider or Class object that is an IQ) + * with the specified element name and name space. The provider will override any providers + * loaded through the classpath. + * + * @param elementName the XML element name. + * @param namespace the XML namespace. + * @param provider the IQ provider. + */ + public void addIQProvider(String elementName, String namespace, + Object provider) + { + if (!(provider instanceof IQProvider || (provider instanceof Class && + IQ.class.isAssignableFrom((Class)provider)))) + { + throw new IllegalArgumentException("Provider must be an IQProvider " + + "or a Class instance."); + } + String key = getProviderKey(elementName, namespace); + iqProviders.put(key, provider); + } + + /** + * Removes the IQ provider with the specified element name and name space. + * + * @param elementName the XML element name. + * @param namespace the XML namespace. + */ + public void removeIQProvider(String elementName, String namespace) { + String key = getProviderKey(elementName, namespace); + iqProviders.remove(key); + } + + /** + * Returns the packet extension provider registered to the specified XML element name + * and namespace. For example, if a provider was registered to the element name "x" and the + * namespace "jabber:x:event", then the following packet would trigger the provider: + * + * <pre> + * <message to='romeo@montague.net' id='message_1'> + * <body>Art thou not Romeo, and a Montague?</body> + * <x xmlns='jabber:x:event'> + * <composing/> + * </x> + * </message></pre> + * + * <p>Note: this method is generally only called by the internal Smack classes. + * + * @param elementName + * @param namespace + * @return the extenion provider. + */ + public Object getExtensionProvider(String elementName, String namespace) { + String key = getProviderKey(elementName, namespace); + return extensionProviders.get(key); + } + + /** + * Adds an extension provider with the specified element name and name space. The provider + * will override any providers loaded through the classpath. The provider must be either + * a PacketExtensionProvider instance, or a Class object of a Javabean. + * + * @param elementName the XML element name. + * @param namespace the XML namespace. + * @param provider the extension provider. + */ + public void addExtensionProvider(String elementName, String namespace, + Object provider) + { + if (!(provider instanceof PacketExtensionProvider || provider instanceof Class)) { + throw new IllegalArgumentException("Provider must be a PacketExtensionProvider " + + "or a Class instance."); + } + String key = getProviderKey(elementName, namespace); + extensionProviders.put(key, provider); + } + + /** + * Removes the extension provider with the specified element name and name space. + * + * @param elementName the XML element name. + * @param namespace the XML namespace. + */ + public void removeExtensionProvider(String elementName, String namespace) { + String key = getProviderKey(elementName, namespace); + extensionProviders.remove(key); + } + + /** + * Returns an Iterator for all PacketExtensionProvider instances. + * + * @return an Iterator for all PacketExtensionProvider instances. + */ + public Iterator getExtensionProviders() { + return Collections.unmodifiableCollection( + new HashMap(extensionProviders).values()).iterator(); + } + + /** + * Returns a String key for a given element name and namespace. + * + * @param elementName the element name. + * @param namespace the namespace. + * @return a unique key for the element name and namespace pair. + */ + protected static String getProviderKey(String elementName, String namespace) { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(elementName).append("/><").append(namespace).append("/>"); + return buf.toString(); + } + + /** + * Returns an array of class loaders to load resources from. + * + * @return an array of ClassLoader instances. + */ + private static ClassLoader[] getClassLoaders() { + ClassLoader[] classLoaders = new ClassLoader[3]; + classLoaders[0] = ProviderManager.class.getClassLoader(); + classLoaders[1] = Thread.currentThread().getContextClassLoader(); + classLoaders[2] = ClassLoader.getSystemClassLoader(); + return classLoaders; + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/package.html new file mode 100644 index 000000000..fccc3836d --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/provider/package.html @@ -0,0 +1 @@ +<body>Provides pluggable parsing of incoming IQ's and packet extensions.</body>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLAnonymous.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLAnonymous.java new file mode 100644 index 000000000..cdb81e08a --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLAnonymous.java @@ -0,0 +1,52 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.sasl; + +import org.jivesoftware.smack.SASLAuthentication; + +/** + * Implementation of the SASL ANONYMOUS mechanisn as defined by the + * <a href="http://www.ietf.org/internet-drafts/draft-ietf-sasl-anon-05.txt">IETF draft + * document</a>. + * + * @author Gaston Dombiak + */ +public class SASLAnonymous extends SASLMechanism { + + public SASLAnonymous(SASLAuthentication saslAuthentication) { + super(saslAuthentication); + } + + protected String getName() { + return "ANONYMOUS"; + } + + protected String getAuthenticationText(String username, String host, String password) { + // Nothing to send in the <auth> body + return null; + } + + protected String getChallengeResponse(byte[] bytes) { + // Some servers may send a challenge to gather more information such as + // email address. Return any string value. + return "anything"; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLMechanism.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLMechanism.java new file mode 100644 index 000000000..b09176755 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLMechanism.java @@ -0,0 +1,122 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.sasl; + +import org.jivesoftware.smack.SASLAuthentication; +import org.jivesoftware.smack.util.StringUtils; + +import java.io.IOException; + +/** + * Base class for SASL mechanisms. Subclasses must implement three methods: + * <ul> + * <li>{@link #getName()} -- returns the common name of the SASL mechanism.</li> + * <li>{@link #getAuthenticationText(String, String, String)} -- authentication text to include + * in the initial <tt>auth</tt> stanza.</li> + * <li>{@link #getChallengeResponse(byte[])} -- to respond challenges made by the server.</li> + * </ul> + * + * @author Gaston Dombiak + */ +public abstract class SASLMechanism { + + private SASLAuthentication saslAuthentication; + + public SASLMechanism(SASLAuthentication saslAuthentication) { + super(); + this.saslAuthentication = saslAuthentication; + } + + /** + * Builds and sends the <tt>auth</tt> stanza to the server. + * + * @param username the username of the user being authenticated. + * @param host the hostname where the user account resides. + * @param password the password of the user. + * @throws IOException If a network error occures while authenticating. + */ + public void authenticate(String username, String host, String password) throws IOException { + // Build the authentication stanza encoding the authentication text + StringBuffer stanza = new StringBuffer(); + stanza.append("<auth mechanism=\"").append(getName()); + stanza.append("\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">"); + String authenticationText = getAuthenticationText(username, host, password); + if (authenticationText != null) { + stanza.append(StringUtils.encodeBase64(authenticationText)); + } + stanza.append("</auth>"); + + // Send the authentication to the server + getSASLAuthentication().send(stanza.toString()); + } + + /** + * The server is challenging the SASL mechanism for the stanza he just sent. Send a + * response to the server's challenge. + * + * @param challenge a base64 encoded string representing the challenge. + */ + public void challengeReceived(String challenge) throws IOException { + // Build the challenge response stanza encoding the response text + StringBuffer stanza = new StringBuffer(); + stanza.append("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">"); + String authenticationText = getChallengeResponse(StringUtils.decodeBase64(challenge)); + if (authenticationText != null) { + stanza.append(StringUtils.encodeBase64(authenticationText)); + } + stanza.append("</response>"); + + // Send the authentication to the server + getSASLAuthentication().send(stanza.toString()); + } + + /** + * Returns the response text to send answering the challenge sent by the server. Mechanisms + * that will never receive a challenge may redefine this method returning <tt>null</tt>. + * + * @param bytes the challenge sent by the server. + * @return the response text to send to answer the challenge sent by the server. + */ + protected abstract String getChallengeResponse(byte[] bytes); + + /** + * Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4. + * + * @return the common name of the SASL mechanism. + */ + protected abstract String getName(); + + /** + * Returns the authentication text to include in the initial <tt>auth</tt> stanza + * or <tt>null</tt> if nothing should be added. + * + * @param username the username of the user being authenticated. + * @param host the hostname where the user account resides. + * @param password the password of the user. + * @return the authentication text to include in the initial <tt>auth</tt> stanza + * or <tt>null</tt> if nothing should be added. + */ + protected abstract String getAuthenticationText(String username, String host, String password); + + protected SASLAuthentication getSASLAuthentication() { + return saslAuthentication; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java new file mode 100644 index 000000000..54d18c607 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java @@ -0,0 +1,58 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.sasl; + +import org.jivesoftware.smack.SASLAuthentication; + +/** + * Implementation of the SASL PLAIN mechanisn as defined by the + * <a href="http://www.ietf.org/internet-drafts/draft-ietf-sasl-plain-08.txt">IETF draft + * document</a>. + * + * @author Gaston Dombiak + */ +public class SASLPlainMechanism extends SASLMechanism { + + public SASLPlainMechanism(SASLAuthentication saslAuthentication) { + super(saslAuthentication); + } + + protected String getName() { + return "PLAIN"; + } + + protected String getAuthenticationText(String username, String host, String password) { + // Build the text containing the "authorization identity" + NUL char + + // "authentication identity" + NUL char + "clear-text password" + StringBuffer text = new StringBuffer(); + text.append(username).append("@").append(host); + text.append('\0'); + text.append(username); + text.append('\0'); + text.append(password); + return text.toString(); + } + + protected String getChallengeResponse(byte[] bytes) { + // Return null since this mechanism will never get a challenge from the server + return null; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/package.html new file mode 100644 index 000000000..1e8cfb784 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/sasl/package.html @@ -0,0 +1 @@ +<body>SASL Mechanisms.</body>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/Cache.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/Cache.java new file mode 100644 index 000000000..fb8a3bcac --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/Cache.java @@ -0,0 +1,628 @@ +/** + * $Revision$ + * $Date$ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.util; + +import java.util.*; + +/** + * A specialized Map that is size-limited (using an LRU algorithm) and + * has an optional expiration time for cache items. The Map is thread-safe.<p> + * + * The algorithm for cache is as follows: a HashMap is maintained for fast + * object lookup. Two linked lists are maintained: one keeps objects in the + * order they are accessed from cache, the other keeps objects in the order + * they were originally added to cache. When objects are added to cache, they + * are first wrapped by a CacheObject which maintains the following pieces + * of information:<ul> + * <li> A pointer to the node in the linked list that maintains accessed + * order for the object. Keeping a reference to the node lets us avoid + * linear scans of the linked list. + * <li> A pointer to the node in the linked list that maintains the age + * of the object in cache. Keeping a reference to the node lets us avoid + * linear scans of the linked list.</ul> + * <p/> + * To get an object from cache, a hash lookup is performed to get a reference + * to the CacheObject that wraps the real object we are looking for. + * The object is subsequently moved to the front of the accessed linked list + * and any necessary cache cleanups are performed. Cache deletion and expiration + * is performed as needed. + * + * @author Matt Tucker + */ +public class Cache implements Map { + + /** + * The map the keys and values are stored in. + */ + protected Map map; + + /** + * Linked list to maintain order that cache objects are accessed + * in, most used to least used. + */ + protected LinkedList lastAccessedList; + + /** + * Linked list to maintain time that cache objects were initially added + * to the cache, most recently added to oldest added. + */ + protected LinkedList ageList; + + /** + * Maximum number of items the cache will hold. + */ + protected int maxCacheSize; + + /** + * Maximum length of time objects can exist in cache before expiring. + */ + protected long maxLifetime; + + /** + * Maintain the number of cache hits and misses. A cache hit occurs every + * time the get method is called and the cache contains the requested + * object. A cache miss represents the opposite occurence.<p> + * + * Keeping track of cache hits and misses lets one measure how efficient + * the cache is; the higher the percentage of hits, the more efficient. + */ + protected long cacheHits, cacheMisses = 0L; + + /** + * Create a new cache and specify the maximum size of for the cache in + * bytes, and the maximum lifetime of objects. + * + * @param maxSize the maximum number of objects the cache will hold. -1 + * means the cache has no max size. + * @param maxLifetime the maximum amount of time (in ms) objects can exist in + * cache before being deleted. -1 means objects never expire. + */ + public Cache(int maxSize, long maxLifetime) { + if (maxSize == 0) { + throw new IllegalArgumentException("Max cache size cannot be 0."); + } + this.maxCacheSize = maxSize; + this.maxLifetime = maxLifetime; + + // Our primary data structure is a hash map. The default capacity of 11 + // is too small in almost all cases, so we set it bigger. + map = new HashMap(103); + + lastAccessedList = new LinkedList(); + ageList = new LinkedList(); + } + + public synchronized Object put(Object key, Object value) { + Object oldValue = null; + // Delete an old entry if it exists. + if (map.containsKey(key)) { + oldValue = remove(key, true); + } + + CacheObject cacheObject = new CacheObject(value); + map.put(key, cacheObject); + // Make an entry into the cache order list. + // Store the cache order list entry so that we can get back to it + // during later lookups. + cacheObject.lastAccessedListNode = lastAccessedList.addFirst(key); + // Add the object to the age list + LinkedListNode ageNode = ageList.addFirst(key); + ageNode.timestamp = System.currentTimeMillis(); + cacheObject.ageListNode = ageNode; + + // If cache is too full, remove least used cache entries until it is not too full. + cullCache(); + + return oldValue; + } + + public synchronized Object get(Object key) { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + CacheObject cacheObject = (CacheObject) map.get(key); + if (cacheObject == null) { + // The object didn't exist in cache, so increment cache misses. + cacheMisses++; + return null; + } + // Remove the object from it's current place in the cache order list, + // and re-insert it at the front of the list. + cacheObject.lastAccessedListNode.remove(); + lastAccessedList.addFirst(cacheObject.lastAccessedListNode); + + // The object exists in cache, so increment cache hits. Also, increment + // the object's read count. + cacheHits++; + cacheObject.readCount++; + + return cacheObject.object; + } + + public synchronized Object remove(Object key) { + return remove(key, false); + } + + /* + * Remove operation with a flag so we can tell coherence if the remove was + * caused by cache internal processing such as eviction or loading + */ + public synchronized Object remove(Object key, boolean internal) { + CacheObject cacheObject = (CacheObject) map.remove(key); + // If the object is not in cache, stop trying to remove it. + if (cacheObject == null) { + return null; + } + // Remove from the cache order list + cacheObject.lastAccessedListNode.remove(); + cacheObject.ageListNode.remove(); + // Remove references to linked list nodes + cacheObject.ageListNode = null; + cacheObject.lastAccessedListNode = null; + + return cacheObject.object; + } + + public synchronized void clear() { + Object[] keys = map.keySet().toArray(); + for (int i = 0; i < keys.length; i++) { + remove(keys[i]); + } + + // Now, reset all containers. + map.clear(); + lastAccessedList.clear(); + ageList.clear(); + + cacheHits = 0; + cacheMisses = 0; + } + + public synchronized int size() { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return map.size(); + } + + public synchronized boolean isEmpty() { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return map.isEmpty(); + } + + public synchronized Collection values() { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + Object[] cacheObjects = map.values().toArray(); + Object[] values = new Object[cacheObjects.length]; + for (int i = 0; i < cacheObjects.length; i++) { + values[i] = ((CacheObject) cacheObjects[i]).object; + } + return Collections.unmodifiableList(Arrays.asList(values)); + } + + public synchronized boolean containsKey(Object key) { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return map.containsKey(key); + } + + public void putAll(Map map) { + for (Iterator i = map.entrySet().iterator(); i.hasNext();) { + Map.Entry entry = (Map.Entry) i.next(); + Object value = entry.getValue(); + // If the map is another DefaultCache instance than the + // entry values will be CacheObject instances that need + // to be converted to the normal object form. + if (value instanceof CacheObject) { + value = ((CacheObject) value).object; + } + put(entry.getKey(), value); + } + } + + public synchronized boolean containsValue(Object value) { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + CacheObject cacheObject = new CacheObject(value); + + return map.containsValue(cacheObject); + } + + public synchronized Set entrySet() { + // Warning -- this method returns CacheObject instances and not Objects + // in the same form they were put into cache. + + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return Collections.unmodifiableSet(map.entrySet()); + } + + public synchronized Set keySet() { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return Collections.unmodifiableSet(map.keySet()); + } + + public long getCacheHits() { + return cacheHits; + } + + public long getCacheMisses() { + return cacheMisses; + } + + public int getMaxCacheSize() { + return maxCacheSize; + } + + public synchronized void setMaxCacheSize(int maxCacheSize) { + this.maxCacheSize = maxCacheSize; + // It's possible that the new max size is smaller than our current cache + // size. If so, we need to delete infrequently used items. + cullCache(); + } + + public long getMaxLifetime() { + return maxLifetime; + } + + public void setMaxLifetime(long maxLifetime) { + this.maxLifetime = maxLifetime; + } + + /** + * Clears all entries out of cache where the entries are older than the + * maximum defined age. + */ + protected synchronized void deleteExpiredEntries() { + // Check if expiration is turned on. + if (maxLifetime <= 0) { + return; + } + + // Remove all old entries. To do this, we remove objects from the end + // of the linked list until they are no longer too old. We get to avoid + // any hash lookups or looking at any more objects than is strictly + // neccessary. + LinkedListNode node = ageList.getLast(); + // If there are no entries in the age list, return. + if (node == null) { + return; + } + + // Determine the expireTime, which is the moment in time that elements + // should expire from cache. Then, we can do an easy check to see + // if the expire time is greater than the expire time. + long expireTime = System.currentTimeMillis() - maxLifetime; + + while (expireTime > node.timestamp) { + if (remove(node.object, true) == null) { + System.err.println("Error attempting to remove(" + node.object.toString() + + ") - cacheObject not found in cache!"); + // remove from the ageList + node.remove(); + } + + // Get the next node. + node = ageList.getLast(); + // If there are no more entries in the age list, return. + if (node == null) { + return; + } + } + } + + /** + * Removes the least recently used elements if the cache size is greater than + * or equal to the maximum allowed size until the cache is at least 10% empty. + */ + protected synchronized void cullCache() { + // Check if a max cache size is defined. + if (maxCacheSize < 0) { + return; + } + + // See if the cache is too big. If so, clean out cache until it's 10% free. + if (map.size() > maxCacheSize) { + // First, delete any old entries to see how much memory that frees. + deleteExpiredEntries(); + // Next, delete the least recently used elements until 10% of the cache + // has been freed. + int desiredSize = (int) (maxCacheSize * .90); + for (int i=map.size(); i>desiredSize; i--) { + // Get the key and invoke the remove method on it. + if (remove(lastAccessedList.getLast().object, true) == null) { + System.err.println("Error attempting to cullCache with remove(" + + lastAccessedList.getLast().object.toString() + ") - " + + "cacheObject not found in cache!"); + lastAccessedList.getLast().remove(); + } + } + } + } + + /** + * Wrapper for all objects put into cache. It's primary purpose is to maintain + * references to the linked lists that maintain the creation time of the object + * and the ordering of the most used objects. + * + * This class is optimized for speed rather than strictly correct encapsulation. + */ + private static class CacheObject { + + /** + * Underlying object wrapped by the CacheObject. + */ + public Object object; + + /** + * A reference to the node in the cache order list. We keep the reference + * here to avoid linear scans of the list. Every time the object is + * accessed, the node is removed from its current spot in the list and + * moved to the front. + */ + public LinkedListNode lastAccessedListNode; + + /** + * A reference to the node in the age order list. We keep the reference + * here to avoid linear scans of the list. The reference is used if the + * object has to be deleted from the list. + */ + public LinkedListNode ageListNode; + + /** + * A count of the number of times the object has been read from cache. + */ + public int readCount = 0; + + /** + * Creates a new cache object wrapper. + * + * @param object the underlying Object to wrap. + */ + public CacheObject(Object object) { + this.object = object; + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CacheObject)) { + return false; + } + + final CacheObject cacheObject = (CacheObject) o; + + if (!object.equals(cacheObject.object)) { + return false; + } + + return true; + } + + public int hashCode() { + return object.hashCode(); + } + } + + /** + * Simple LinkedList implementation. The main feature is that list nodes + * are public, which allows very fast delete operations when one has a + * reference to the node that is to be deleted.<p> + */ + private static class LinkedList { + + /** + * The root of the list keeps a reference to both the first and last + * elements of the list. + */ + private LinkedListNode head = new LinkedListNode("head", null, null); + + /** + * Creates a new linked list. + */ + public LinkedList() { + head.next = head.previous = head; + } + + /** + * Returns the first linked list node in the list. + * + * @return the first element of the list. + */ + public LinkedListNode getFirst() { + LinkedListNode node = head.next; + if (node == head) { + return null; + } + return node; + } + + /** + * Returns the last linked list node in the list. + * + * @return the last element of the list. + */ + public LinkedListNode getLast() { + LinkedListNode node = head.previous; + if (node == head) { + return null; + } + return node; + } + + /** + * Adds a node to the beginning of the list. + * + * @param node the node to add to the beginning of the list. + */ + public LinkedListNode addFirst(LinkedListNode node) { + node.next = head.next; + node.previous = head; + node.previous.next = node; + node.next.previous = node; + return node; + } + + /** + * Adds an object to the beginning of the list by automatically creating a + * a new node and adding it to the beginning of the list. + * + * @param object the object to add to the beginning of the list. + * @return the node created to wrap the object. + */ + public LinkedListNode addFirst(Object object) { + LinkedListNode node = new LinkedListNode(object, head.next, head); + node.previous.next = node; + node.next.previous = node; + return node; + } + + /** + * Adds an object to the end of the list by automatically creating a + * a new node and adding it to the end of the list. + * + * @param object the object to add to the end of the list. + * @return the node created to wrap the object. + */ + public LinkedListNode addLast(Object object) { + LinkedListNode node = new LinkedListNode(object, head, head.previous); + node.previous.next = node; + node.next.previous = node; + return node; + } + + /** + * Erases all elements in the list and re-initializes it. + */ + public void clear() { + //Remove all references in the list. + LinkedListNode node = getLast(); + while (node != null) { + node.remove(); + node = getLast(); + } + + //Re-initialize. + head.next = head.previous = head; + } + + /** + * Returns a String representation of the linked list with a comma + * delimited list of all the elements in the list. + * + * @return a String representation of the LinkedList. + */ + public String toString() { + LinkedListNode node = head.next; + StringBuffer buf = new StringBuffer(); + while (node != head) { + buf.append(node.toString()).append(", "); + node = node.next; + } + return buf.toString(); + } + } + + /** + * Doubly linked node in a LinkedList. Most LinkedList implementations keep the + * equivalent of this class private. We make it public so that references + * to each node in the list can be maintained externally. + * + * Exposing this class lets us make remove operations very fast. Remove is + * built into this class and only requires two reference reassignments. If + * remove existed in the main LinkedList class, a linear scan would have to + * be performed to find the correct node to delete. + * + * The linked list implementation was specifically written for the Jive + * cache system. While it can be used as a general purpose linked list, for + * most applications, it is more suitable to use the linked list that is part + * of the Java Collections package. + */ + private static class LinkedListNode { + + public LinkedListNode previous; + public LinkedListNode next; + public Object object; + + /** + * This class is further customized for the Jive cache system. It + * maintains a timestamp of when a Cacheable object was first added to + * cache. Timestamps are stored as long values and represent the number + * of milliseconds passed since January 1, 1970 00:00:00.000 GMT.<p> + * + * The creation timestamp is used in the case that the cache has a + * maximum lifetime set. In that case, when + * [current time] - [creation time] > [max lifetime], the object will be + * deleted from cache. + */ + public long timestamp; + + /** + * Constructs a new linked list node. + * + * @param object the Object that the node represents. + * @param next a reference to the next LinkedListNode in the list. + * @param previous a reference to the previous LinkedListNode in the list. + */ + public LinkedListNode(Object object, LinkedListNode next, + LinkedListNode previous) + { + this.object = object; + this.next = next; + this.previous = previous; + } + + /** + * Removes this node from the linked list that it is a part of. + */ + public void remove() { + previous.next = next; + next.previous = previous; + } + + /** + * Returns a String representation of the linked list node by calling the + * toString method of the node's object. + * + * @return a String representation of the LinkedListNode. + */ + public String toString() { + return object.toString(); + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/DNSUtil.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/DNSUtil.java new file mode 100644 index 000000000..bf0d7cf3f --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/DNSUtil.java @@ -0,0 +1,219 @@ +/** + * $Revision$ + * $Date$ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.util; + +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.Attributes; +import java.util.Hashtable; +import java.util.Map; + +/** + * Utilty class to perform DNS lookups for XMPP services. + * + * @author Matt Tucker + */ +public class DNSUtil { + + /** + * Create a cache to hold the 100 most recently accessed DNS lookups for a period of + * 10 minutes. + */ + private static Map cache = new Cache(100, 1000*60*10); + + private static DirContext context; + + static { + try { + Hashtable env = new Hashtable(); + env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + context = new InitialDirContext(env); + } + catch (Exception e) { + // Ignore. + } + } + + /** + * Returns the host name and port that the specified XMPP server can be + * reached at for client-to-server communication. A DNS lookup for a SRV + * record in the form "_xmpp-client._tcp.example.com" is attempted, according + * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form + * of "_jabber._tcp.example.com" is attempted since servers that implement an + * older version of the protocol may be listed using that notation. If that + * lookup fails as well, it's assumed that the XMPP server lives at the + * host resolved by a DNS lookup at the specified domain on the default port + * of 5222.<p> + * + * As an example, a lookup for "example.com" may return "im.example.com:5269". + * + * @param domain the domain. + * @return a HostAddress, which encompasses the hostname and port that the XMPP + * server can be reached at for the specified domain. + */ + public static HostAddress resolveXMPPDomain(String domain) { + if (context == null) { + return new HostAddress(domain, 5222); + } + String key = "c" + domain; + // Return item from cache if it exists. + if (cache.containsKey(key)) { + HostAddress address = (HostAddress)cache.get(key); + if (address != null) { + return address; + } + } + String host = domain; + int port = 5222; + try { + Attributes dnsLookup = context.getAttributes("_xmpp-client._tcp." + domain); + String srvRecord = (String)dnsLookup.get("SRV").get(); + String [] srvRecordEntries = srvRecord.split(" "); + port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); + host = srvRecordEntries[srvRecordEntries.length-1]; + } + catch (Exception e) { + // Ignore. + } + // Host entries in DNS should end with a ".". + if (host.endsWith(".")) { + host = host.substring(0, host.length()-1); + } + HostAddress address = new HostAddress(host, port); + // Add item to cache. + cache.put(key, address); + return address; + } + + /** + * Returns the host name and port that the specified XMPP server can be + * reached at for server-to-server communication. A DNS lookup for a SRV + * record in the form "_xmpp-server._tcp.example.com" is attempted, according + * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form + * of "_jabber._tcp.example.com" is attempted since servers that implement an + * older version of the protocol may be listed using that notation. If that + * lookup fails as well, it's assumed that the XMPP server lives at the + * host resolved by a DNS lookup at the specified domain on the default port + * of 5269.<p> + * + * As an example, a lookup for "example.com" may return "im.example.com:5269". + * + * @param domain the domain. + * @return a HostAddress, which encompasses the hostname and port that the XMPP + * server can be reached at for the specified domain. + */ + public static HostAddress resolveXMPPServerDomain(String domain) { + if (context == null) { + return new HostAddress(domain, 5269); + } + String key = "s" + domain; + // Return item from cache if it exists. + if (cache.containsKey(key)) { + HostAddress address = (HostAddress)cache.get(key); + if (address != null) { + return address; + } + } + String host = domain; + int port = 5269; + try { + Attributes dnsLookup = context.getAttributes("_xmpp-server._tcp." + domain); + String srvRecord = (String)dnsLookup.get("SRV").get(); + String [] srvRecordEntries = srvRecord.split(" "); + port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); + host = srvRecordEntries[srvRecordEntries.length-1]; + } + catch (Exception e) { + // Attempt lookup with older "jabber" name. + try { + Attributes dnsLookup = context.getAttributes("_jabber._tcp." + domain); + String srvRecord = (String)dnsLookup.get("SRV").get(); + String [] srvRecordEntries = srvRecord.split(" "); + port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); + host = srvRecordEntries[srvRecordEntries.length-1]; + } + catch (Exception e2) { } + } + // Host entries in DNS should end with a ".". + if (host.endsWith(".")) { + host = host.substring(0, host.length()-1); + } + HostAddress address = new HostAddress(host, port); + // Add item to cache. + cache.put(key, address); + return address; + } + + /** + * Encapsulates a hostname and port. + */ + public static class HostAddress { + + private String host; + private int port; + + private HostAddress(String host, int port) { + this.host = host; + this.port = port; + } + + /** + * Returns the hostname. + * + * @return the hostname. + */ + public String getHost() { + return host; + } + + /** + * Returns the port. + * + * @return the port. + */ + public int getPort() { + return port; + } + + public String toString() { + return host + ":" + port; + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HostAddress)) { + return false; + } + + final HostAddress address = (HostAddress) o; + + if (!host.equals(address.host)) { + return false; + } + if (port != address.port) { + return false; + } + + return true; + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableReader.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableReader.java new file mode 100644 index 000000000..5b073b830 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableReader.java @@ -0,0 +1,118 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.util; + +import java.io.*; +import java.util.*; + +/** + * An ObservableReader is a wrapper on a Reader that notifies to its listeners when + * reading character streams. + * + * @author Gaston Dombiak + */ +public class ObservableReader extends Reader { + + Reader wrappedReader = null; + List listeners = new ArrayList(); + + public ObservableReader(Reader wrappedReader) { + this.wrappedReader = wrappedReader; + } + + public int read(char[] cbuf, int off, int len) throws IOException { + int count = wrappedReader.read(cbuf, off, len); + if (count > 0) { + String str = new String(cbuf, off, count); + // Notify that a new string has been read + ReaderListener[] readerListeners = null; + synchronized (listeners) { + readerListeners = new ReaderListener[listeners.size()]; + listeners.toArray(readerListeners); + } + for (int i = 0; i < readerListeners.length; i++) { + readerListeners[i].read(str); + } + } + return count; + } + + public void close() throws IOException { + wrappedReader.close(); + } + + public int read() throws IOException { + return wrappedReader.read(); + } + + public int read(char cbuf[]) throws IOException { + return wrappedReader.read(cbuf); + } + + public long skip(long n) throws IOException { + return wrappedReader.skip(n); + } + + public boolean ready() throws IOException { + return wrappedReader.ready(); + } + + public boolean markSupported() { + return wrappedReader.markSupported(); + } + + public void mark(int readAheadLimit) throws IOException { + wrappedReader.mark(readAheadLimit); + } + + public void reset() throws IOException { + wrappedReader.reset(); + } + + /** + * Adds a reader listener to this reader that will be notified when + * new strings are read. + * + * @param readerListener a reader listener. + */ + public void addReaderListener(ReaderListener readerListener) { + if (readerListener == null) { + return; + } + synchronized (listeners) { + if (!listeners.contains(readerListener)) { + listeners.add(readerListener); + } + } + } + + /** + * Removes a reader listener from this reader. + * + * @param readerListener a reader listener. + */ + public void removeReaderListener(ReaderListener readerListener) { + synchronized (listeners) { + listeners.remove(readerListener); + } + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableWriter.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableWriter.java new file mode 100644 index 000000000..ea1c0356e --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ObservableWriter.java @@ -0,0 +1,120 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.util; + +import java.io.*; +import java.util.*; + +/** + * An ObservableWriter is a wrapper on a Writer that notifies to its listeners when + * writing to character streams. + * + * @author Gaston Dombiak + */ +public class ObservableWriter extends Writer { + + Writer wrappedWriter = null; + List listeners = new ArrayList(); + + public ObservableWriter(Writer wrappedWriter) { + this.wrappedWriter = wrappedWriter; + } + + public void write(char cbuf[], int off, int len) throws IOException { + wrappedWriter.write(cbuf, off, len); + String str = new String(cbuf, off, len); + notifyListeners(str); + } + + public void flush() throws IOException { + wrappedWriter.flush(); + } + + public void close() throws IOException { + wrappedWriter.close(); + } + + public void write(int c) throws IOException { + wrappedWriter.write(c); + } + + public void write(char cbuf[]) throws IOException { + wrappedWriter.write(cbuf); + String str = new String(cbuf); + notifyListeners(str); + } + + public void write(String str) throws IOException { + wrappedWriter.write(str); + notifyListeners(str); + } + + public void write(String str, int off, int len) throws IOException { + wrappedWriter.write(str, off, len); + str = str.substring(off, off + len); + notifyListeners(str); + } + + /** + * Notify that a new string has been written. + * + * @param str the written String to notify + */ + private void notifyListeners(String str) { + WriterListener[] writerListeners = null; + synchronized (listeners) { + writerListeners = new WriterListener[listeners.size()]; + listeners.toArray(writerListeners); + } + for (int i = 0; i < writerListeners.length; i++) { + writerListeners[i].write(str); + } + } + + /** + * Adds a writer listener to this writer that will be notified when + * new strings are sent. + * + * @param writerListener a writer listener. + */ + public void addWriterListener(WriterListener writerListener) { + if (writerListener == null) { + return; + } + synchronized (listeners) { + if (!listeners.contains(writerListener)) { + listeners.add(writerListener); + } + } + } + + /** + * Removes a writer listener from this writer. + * + * @param writerListener a writer listener. + */ + public void removeWriterListener(WriterListener writerListener) { + synchronized (listeners) { + listeners.remove(writerListener); + } + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/PacketParserUtils.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/PacketParserUtils.java new file mode 100644 index 000000000..842c0a34d --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/PacketParserUtils.java @@ -0,0 +1,417 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.util; + +import java.beans.PropertyDescriptor; +import java.util.Map; +import java.util.Iterator; +import java.util.HashMap; +import java.io.ObjectInputStream; +import java.io.ByteArrayInputStream; + +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smack.provider.ProviderManager; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * Utility class that helps to parse packets. Any parsing packets method that must be shared + * between many clients must be placed in this utility class. + * + * @author Gaston Dombiak + */ +public class PacketParserUtils { + + /** + * Namespace used to store packet properties. + */ + private static final String PROPERTIES_NAMESPACE = + "http://www.jivesoftware.com/xmlns/xmpp/properties"; + + /** + * Parses a message packet. + * + * @param parser the XML parser, positioned at the start of a message packet. + * @return a Message packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static Packet parseMessage(XmlPullParser parser) throws Exception { + Message message = new Message(); + String id = parser.getAttributeValue("", "id"); + message.setPacketID(id == null ? Packet.ID_NOT_AVAILABLE : id); + message.setTo(parser.getAttributeValue("", "to")); + message.setFrom(parser.getAttributeValue("", "from")); + message.setType(Message.Type.fromString(parser.getAttributeValue("", "type"))); + + // Parse sub-elements. We include extra logic to make sure the values + // are only read once. This is because it's possible for the names to appear + // in arbitrary sub-elements. + boolean done = false; + String subject = null; + String body = null; + String thread = null; + Map properties = null; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + if (elementName.equals("subject")) { + if (subject == null) { + subject = parser.nextText(); + } + } + else if (elementName.equals("body")) { + if (body == null) { + body = parser.nextText(); + } + } + else if (elementName.equals("thread")) { + if (thread == null) { + thread = parser.nextText(); + } + } + else if (elementName.equals("error")) { + message.setError(parseError(parser)); + } + else if (elementName.equals("properties") && + namespace.equals(PROPERTIES_NAMESPACE)) + { + properties = parseProperties(parser); + } + // Otherwise, it must be a packet extension. + else { + message.addExtension( + PacketParserUtils.parsePacketExtension(elementName, namespace, parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("message")) { + done = true; + } + } + } + message.setSubject(subject); + message.setBody(body); + message.setThread(thread); + // Set packet properties. + if (properties != null) { + for (Iterator i=properties.keySet().iterator(); i.hasNext(); ) { + String name = (String)i.next(); + message.setProperty(name, properties.get(name)); + } + } + return message; + } + + /** + * Parses a presence packet. + * + * @param parser the XML parser, positioned at the start of a presence packet. + * @return a Presence packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static Presence parsePresence(XmlPullParser parser) throws Exception { + Presence.Type type = Presence.Type.fromString(parser.getAttributeValue("", "type")); + + Presence presence = new Presence(type); + presence.setTo(parser.getAttributeValue("", "to")); + presence.setFrom(parser.getAttributeValue("", "from")); + String id = parser.getAttributeValue("", "id"); + presence.setPacketID(id == null ? Packet.ID_NOT_AVAILABLE : id); + + // Parse sub-elements + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + if (elementName.equals("status")) { + presence.setStatus(parser.nextText()); + } + else if (elementName.equals("priority")) { + try { + int priority = Integer.parseInt(parser.nextText()); + presence.setPriority(priority); + } + catch (NumberFormatException nfe) { } + catch (IllegalArgumentException iae) { + // Presence priority is out of range so assume priority to be zero + presence.setPriority(0); + } + } + else if (elementName.equals("show")) { + presence.setMode(Presence.Mode.fromString(parser.nextText())); + } + else if (elementName.equals("error")) { + presence.setError(parseError(parser)); + } + else if (elementName.equals("properties") && + namespace.equals(PROPERTIES_NAMESPACE)) + { + Map properties = parseProperties(parser); + // Set packet properties. + for (Iterator i=properties.keySet().iterator(); i.hasNext(); ) { + String name = (String)i.next(); + presence.setProperty(name, properties.get(name)); + } + } + // Otherwise, it must be a packet extension. + else { + presence.addExtension( + PacketParserUtils.parsePacketExtension(elementName, namespace, parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("presence")) { + done = true; + } + } + } + return presence; + } + + /** + * Parse a properties sub-packet. If any errors occur while de-serializing Java object + * properties, an exception will be printed and not thrown since a thrown + * exception will shut down the entire connection. ClassCastExceptions will occur + * when both the sender and receiver of the packet don't have identical versions + * of the same class. + * + * @param parser the XML parser, positioned at the start of a properties sub-packet. + * @return a map of the properties. + * @throws Exception if an error occurs while parsing the properties. + */ + public static Map parseProperties(XmlPullParser parser) throws Exception { + Map properties = new HashMap(); + while (true) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG && parser.getName().equals("property")) { + // Parse a property + boolean done = false; + String name = null; + String type = null; + String valueText = null; + Object value = null; + while (!done) { + eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + if (elementName.equals("name")) { + name = parser.nextText(); + } + else if (elementName.equals("value")) { + type = parser.getAttributeValue("", "type"); + valueText = parser.nextText(); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("property")) { + if ("integer".equals(type)) { + value = new Integer(valueText); + } + else if ("long".equals(type)) { + value = new Long(valueText); + } + else if ("float".equals(type)) { + value = new Float(valueText); + } + else if ("double".equals(type)) { + value = new Double(valueText); + } + else if ("boolean".equals(type)) { + value = new Boolean(valueText); + } + else if ("string".equals(type)) { + value = valueText; + } + else if ("java-object".equals(type)) { + try { + byte [] bytes = StringUtils.decodeBase64(valueText); + ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes)); + value = in.readObject(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + if (name != null && value != null) { + properties.put(name, value); + } + done = true; + } + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("properties")) { + break; + } + } + } + return properties; + } + + /** + * Parses error sub-packets. + * + * @param parser the XML parser. + * @return an error sub-packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static XMPPError parseError(XmlPullParser parser) throws Exception { + String errorCode = "-1"; + String message = null; + for (int i=0; i<parser.getAttributeCount(); i++) { + if (parser.getAttributeName(i).equals("code")) { + errorCode = parser.getAttributeValue("", "code"); + } + } + // Get the error text in a safe way since we are not sure about the error message format + try { + message = parser.nextText(); + } + catch (XmlPullParserException ex) {} + while (true) { + if (parser.getEventType() == XmlPullParser.END_TAG && parser.getName().equals("error")) { + break; + } + parser.next(); + } + return new XMPPError(Integer.parseInt(errorCode), message); + } + + /** + * Parses a packet extension sub-packet. + * + * @param elementName the XML element name of the packet extension. + * @param namespace the XML namespace of the packet extension. + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public static PacketExtension parsePacketExtension(String elementName, String namespace, XmlPullParser parser) + throws Exception + { + // See if a provider is registered to handle the extension. + Object provider = ProviderManager.getDefault().getExtensionProvider(elementName, namespace); + if (provider != null) { + if (provider instanceof PacketExtensionProvider) { + return ((PacketExtensionProvider)provider).parseExtension(parser); + } + else if (provider instanceof Class) { + return (PacketExtension)parseWithIntrospection( + elementName, (Class)provider, parser); + } + } + // No providers registered, so use a default extension. + DefaultPacketExtension extension = new DefaultPacketExtension(elementName, namespace); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String name = parser.getName(); + // If an empty element, set the value with the empty string. + if (parser.isEmptyElementTag()) { + extension.setValue(name,""); + } + // Otherwise, get the the element text. + else { + eventType = parser.next(); + if (eventType == XmlPullParser.TEXT) { + String value = parser.getText(); + extension.setValue(name, value); + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(elementName)) { + done = true; + } + } + } + return extension; + } + + public static Object parseWithIntrospection(String elementName, + Class objectClass, XmlPullParser parser) throws Exception + { + boolean done = false; + Object object = objectClass.newInstance(); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String name = parser.getName(); + String stringValue = parser.nextText(); + PropertyDescriptor descriptor = new PropertyDescriptor(name, objectClass); + // Load the class type of the property. + Class propertyType = descriptor.getPropertyType(); + // Get the value of the property by converting it from a + // String to the correct object type. + Object value = decode(propertyType, stringValue); + // Set the value of the bean. + descriptor.getWriteMethod().invoke(object, new Object[] { value }); + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(elementName)) { + done = true; + } + } + } + return object; + } + + /** + * Decodes a String into an object of the specified type. If the object + * type is not supported, null will be returned. + * + * @param type the type of the property. + * @param value the encode String value to decode. + * @return the String value decoded into the specified type. + */ + private static Object decode(Class type, String value) throws Exception { + if (type.getName().equals("java.lang.String")) { + return value; + } + if (type.getName().equals("boolean")) { + return Boolean.valueOf(value); + } + if (type.getName().equals("int")) { + return Integer.valueOf(value); + } + if (type.getName().equals("long")) { + return Long.valueOf(value); + } + if (type.getName().equals("float")) { + return Float.valueOf(value); + } + if (type.getName().equals("double")) { + return Double.valueOf(value); + } + if (type.getName().equals("java.lang.Class")) { + return Class.forName(value); + } + return null; + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ReaderListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ReaderListener.java new file mode 100644 index 000000000..1b818e9b3 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/ReaderListener.java @@ -0,0 +1,41 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.util; + +/** + * Interface that allows for implementing classes to listen for string reading + * events. Listeners are registered with ObservableReader objects. + * + * @see ObservableReader#addReaderListener + * @see ObservableReader#removeReaderListener + * + * @author Gaston Dombiak + */ +public interface ReaderListener { + + /** + * Notification that the Reader has read a new string. + * + * @param str the read String + */ + public abstract void read(String str); + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/StringUtils.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/StringUtils.java new file mode 100644 index 000000000..b8a32959c --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/StringUtils.java @@ -0,0 +1,432 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.io.UnsupportedEncodingException; +import java.util.Random; + +/** + * A collection of utility methods for String objects. + */ +public class StringUtils { + + private static final char[] QUOTE_ENCODE = """.toCharArray(); + private static final char[] AMP_ENCODE = "&".toCharArray(); + private static final char[] LT_ENCODE = "<".toCharArray(); + private static final char[] GT_ENCODE = ">".toCharArray(); + + /** + * Returns the name portion of a XMPP address. For example, for the + * address "matt@jivesoftware.com/Smack", "matt" would be returned. If no + * username is present in the address, the empty string will be returned. + * + * @param XMPPAddress the XMPP address. + * @return the name portion of the XMPP address. + */ + public static String parseName(String XMPPAddress) { + if (XMPPAddress == null) { + return null; + } + int atIndex = XMPPAddress.indexOf("@"); + if (atIndex <= 0) { + return ""; + } + else { + return XMPPAddress.substring(0, atIndex); + } + } + + /** + * Returns the server portion of a XMPP address. For example, for the + * address "matt@jivesoftware.com/Smack", "jivesoftware.com" would be returned. + * If no server is present in the address, the empty string will be returned. + * + * @param XMPPAddress the XMPP address. + * @return the server portion of the XMPP address. + */ + public static String parseServer(String XMPPAddress) { + if (XMPPAddress == null) { + return null; + } + int atIndex = XMPPAddress.indexOf("@"); + // If the String ends with '@', return the empty string. + if (atIndex + 1 > XMPPAddress.length()) { + return ""; + } + int slashIndex = XMPPAddress.indexOf("/"); + if (slashIndex > 0) { + return XMPPAddress.substring(atIndex + 1, slashIndex); + } + else { + return XMPPAddress.substring(atIndex + 1); + } + } + + /** + * Returns the resource portion of a XMPP address. For example, for the + * address "matt@jivesoftware.com/Smack", "Smack" would be returned. If no + * resource is present in the address, the empty string will be returned. + * + * @param XMPPAddress the XMPP address. + * @return the resource portion of the XMPP address. + */ + public static String parseResource(String XMPPAddress) { + if (XMPPAddress == null) { + return null; + } + int slashIndex = XMPPAddress.indexOf("/"); + if (slashIndex + 1 > XMPPAddress.length() || slashIndex < 0) { + return ""; + } + else { + return XMPPAddress.substring(slashIndex + 1); + } + } + + /** + * Returns the XMPP address with any resource information removed. For example, + * for the address "matt@jivesoftware.com/Smack", "matt@jivesoftware.com" would + * be returned. + * + * @param XMPPAddress the XMPP address. + * @return the bare XMPP address without resource information. + */ + public static String parseBareAddress(String XMPPAddress) { + if (XMPPAddress == null) { + return null; + } + int slashIndex = XMPPAddress.indexOf("/"); + if (slashIndex < 0) { + return XMPPAddress; + } + else if (slashIndex == 0) { + return ""; + } + else { + return XMPPAddress.substring(0, slashIndex); + } + } + + /** + * Escapes all necessary characters in the String so that it can be used + * in an XML doc. + * + * @param string the string to escape. + * @return the string with appropriate characters escaped. + */ + public static String escapeForXML(String string) { + if (string == null) { + return null; + } + char ch; + int i=0; + int last=0; + char[] input = string.toCharArray(); + int len = input.length; + StringBuffer out = new StringBuffer((int)(len*1.3)); + for (; i < len; i++) { + ch = input[i]; + if (ch > '>') { + continue; + } + else if (ch == '<') { + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + out.append(LT_ENCODE); + } + else if (ch == '>') { + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + out.append(GT_ENCODE); + } + + else if (ch == '&') { + if (i > last) { + out.append(input, last, i - last); + } + // Do nothing if the string is of the form ë (unicode value) + if (!(len > i + 5 + && input[i + 1] == '#' + && Character.isDigit(input[i + 2]) + && Character.isDigit(input[i + 3]) + && Character.isDigit(input[i + 4]) + && input[i + 5] == ';')) { + last = i + 1; + out.append(AMP_ENCODE); + } + } + else if (ch == '"') { + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + out.append(QUOTE_ENCODE); + } + } + if (last == 0) { + return string; + } + if (i > last) { + out.append(input, last, i - last); + } + return out.toString(); + } + + /** + * Used by the hash method. + */ + private static MessageDigest digest = null; + + /** + * Hashes a String using the SHA-1 algorithm and returns the result as a + * String of hexadecimal numbers. This method is synchronized to avoid + * excessive MessageDigest object creation. If calling this method becomes + * a bottleneck in your code, you may wish to maintain a pool of + * MessageDigest objects instead of using this method. + * <p> + * A hash is a one-way function -- that is, given an + * input, an output is easily computed. However, given the output, the + * input is almost impossible to compute. This is useful for passwords + * since we can store the hash and a hacker will then have a very hard time + * determining the original password. + * + * @param data the String to compute the hash of. + * @return a hashed version of the passed-in String + */ + public synchronized static String hash(String data) { + if (digest == null) { + try { + digest = MessageDigest.getInstance("SHA-1"); + } + catch (NoSuchAlgorithmException nsae) { + System.err.println("Failed to load the SHA-1 MessageDigest. " + + "Jive will be unable to function normally."); + } + } + // Now, compute hash. + try { + digest.update(data.getBytes("UTF-8")); + } + catch (UnsupportedEncodingException e) { + System.err.println(e); + } + return encodeHex(digest.digest()); + } + + /** + * Encodes an array of bytes as String representation of hexadecimal. + * + * @param bytes an array of bytes to convert to a hex string. + * @return generated hex string. + */ + public static String encodeHex(byte[] bytes) { + StringBuffer hex = new StringBuffer(bytes.length * 2); + + for (int i=0; i<bytes.length; i++) { + if (((int) bytes[i] & 0xff) < 0x10) { + hex.append("0"); + } + hex.append(Integer.toString((int) bytes[i] & 0xff, 16)); + } + + return hex.toString(); + } + + //********************************************************************* + //* Base64 - a simple base64 encoder and decoder. + //* + //* Copyright (c) 1999, Bob Withers - bwit@pobox.com + //* + //* This code may be freely used for any purpose, either personal + //* or commercial, provided the authors copyright notice remains + //* intact. + //********************************************************************* + + private static final int fillchar = '='; + private static final String cvt = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + "0123456789+/"; + + /** + * Encodes a String as a base64 String. + * + * @param data a String to encode. + * @return a base64 encoded String. + */ + public static String encodeBase64(String data) { + byte [] bytes = null; + try { + bytes = data.getBytes("ISO-8859-1"); + } + catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); + } + return encodeBase64(bytes); + } + + /** + * Encodes a byte array into a base64 String. + * + * @param data a byte array to encode. + * @return a base64 encode String. + */ + public static String encodeBase64(byte[] data) { + int c; + int len = data.length; + StringBuffer ret = new StringBuffer(((len / 3) + 1) * 4); + for (int i = 0; i < len; ++i) { + c = (data[i] >> 2) & 0x3f; + ret.append(cvt.charAt(c)); + c = (data[i] << 4) & 0x3f; + if (++i < len) + c |= (data[i] >> 4) & 0x0f; + + ret.append(cvt.charAt(c)); + if (i < len) { + c = (data[i] << 2) & 0x3f; + if (++i < len) + c |= (data[i] >> 6) & 0x03; + + ret.append(cvt.charAt(c)); + } + else { + ++i; + ret.append((char) fillchar); + } + + if (i < len) { + c = data[i] & 0x3f; + ret.append(cvt.charAt(c)); + } + else { + ret.append((char) fillchar); + } + } + return ret.toString(); + } + + /** + * Decodes a base64 String. + * + * @param data a base64 encoded String to decode. + * @return the decoded String. + */ + public static byte[] decodeBase64(String data) { + byte [] bytes = null; + try { + bytes = data.getBytes("ISO-8859-1"); + return decodeBase64(bytes).getBytes("ISO-8859-1"); + } + catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); + } + return new byte[] { }; + } + + /** + * Decodes a base64 aray of bytes. + * + * @param data a base64 encode byte array to decode. + * @return the decoded String. + */ + private static String decodeBase64(byte[] data) { + int c, c1; + int len = data.length; + StringBuffer ret = new StringBuffer((len * 3) / 4); + for (int i = 0; i < len; ++i) { + c = cvt.indexOf(data[i]); + ++i; + c1 = cvt.indexOf(data[i]); + c = ((c << 2) | ((c1 >> 4) & 0x3)); + ret.append((char) c); + if (++i < len) { + c = data[i]; + if (fillchar == c) + break; + + c = cvt.indexOf(c); + c1 = ((c1 << 4) & 0xf0) | ((c >> 2) & 0xf); + ret.append((char) c1); + } + + if (++i < len) { + c1 = data[i]; + if (fillchar == c1) + break; + + c1 = cvt.indexOf(c1); + c = ((c << 6) & 0xc0) | c1; + ret.append((char) c); + } + } + return ret.toString(); + } + + /** + * Pseudo-random number generator object for use with randomString(). + * The Random class is not considered to be cryptographically secure, so + * only use these random Strings for low to medium security applications. + */ + private static Random randGen = new Random(); + + /** + * Array of numbers and letters of mixed case. Numbers appear in the list + * twice so that there is a more equal chance that a number will be picked. + * We can use the array to get a random number or letter by picking a random + * array index. + */ + private static char[] numbersAndLetters = ("0123456789abcdefghijklmnopqrstuvwxyz" + + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray(); + + /** + * Returns a random String of numbers and letters (lower and upper case) + * of the specified length. The method uses the Random class that is + * built-in to Java which is suitable for low to medium grade security uses. + * This means that the output is only pseudo random, i.e., each number is + * mathematically generated so is not truly random.<p> + * + * The specified length must be at least one. If not, the method will return + * null. + * + * @param length the desired length of the random String to return. + * @return a random String of numbers and letters of the specified length. + */ + public static final String randomString(int length) { + if (length < 1) { + return null; + } + // Create a char buffer to put random letters and numbers in. + char [] randBuffer = new char[length]; + for (int i=0; i<randBuffer.length; i++) { + randBuffer[i] = numbersAndLetters[randGen.nextInt(71)]; + } + return new String(randBuffer); + } + + private StringUtils() { + // Not instantiable. + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/WriterListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/WriterListener.java new file mode 100644 index 000000000..175f3a66d --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/WriterListener.java @@ -0,0 +1,41 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smack.util; + +/** + * Interface that allows for implementing classes to listen for string writing + * events. Listeners are registered with ObservableWriter objects. + * + * @see ObservableWriter#addWriterListener + * @see ObservableWriter#removeWriterListener + * + * @author Gaston Dombiak + */ +public interface WriterListener { + + /** + * Notification that the Writer has written a new string. + * + * @param str the written string + */ + public abstract void write(String str); + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/package.html new file mode 100644 index 000000000..e34bfe316 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smack/util/package.html @@ -0,0 +1 @@ +<body>Utility classes.</body>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/DefaultMessageEventRequestListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/DefaultMessageEventRequestListener.java new file mode 100644 index 000000000..5cfb15acf --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/DefaultMessageEventRequestListener.java @@ -0,0 +1,55 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +/** + * + * Default implementation of the MessageEventRequestListener interface.<p> + * + * This class automatically sends a delivered notification to the sender of the message + * if the sender has requested to be notified when the message is delivered. + * + * @author Gaston Dombiak + */ +public class DefaultMessageEventRequestListener implements MessageEventRequestListener { + + public void deliveredNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager) + { + // Send to the message's sender that the message has been delivered + messageEventManager.sendDeliveredNotification(from, packetID); + } + + public void displayedNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager) + { + } + + public void composingNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager) + { + } + + public void offlineNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager) + { + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/Form.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/Form.java new file mode 100644 index 000000000..0f9ae8084 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/Form.java @@ -0,0 +1,539 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import java.util.*; + +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smackx.packet.DataForm; + +/** + * Represents a Form for gathering data. The form could be of the following types: + * <ul> + * <li>form -> Indicates a form to fill out.</li> + * <li>submit -> The form is filled out, and this is the data that is being returned from + * the form.</li> + * <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li> + * <li>result -> Data results being returned from a search, or some other query.</li> + * </ul> + * + * Depending of the form's type different operations are available. For example, it's only possible + * to set answers if the form is of type "submit". + * + * @author Gaston Dombiak + */ +public class Form { + + public static final String TYPE_FORM = "form"; + public static final String TYPE_SUBMIT = "submit"; + public static final String TYPE_CANCEL = "cancel"; + public static final String TYPE_RESULT = "result"; + + private DataForm dataForm; + + /** + * Returns a new ReportedData if the packet is used for gathering data and includes an + * extension that matches the elementName and namespace "x","jabber:x:data". + * + * @param packet the packet used for gathering data. + */ + public static Form getFormFrom(Packet packet) { + // Check if the packet includes the DataForm extension + PacketExtension packetExtension = packet.getExtension("x","jabber:x:data"); + if (packetExtension != null) { + // Check if the existing DataForm is not a result of a search + DataForm dataForm = (DataForm) packetExtension; + if (dataForm.getReportedData() == null) + return new Form(dataForm); + } + // Otherwise return null + return null; + } + + /** + * Creates a new Form that will wrap an existing DataForm. The wrapped DataForm must be + * used for gathering data. + * + * @param dataForm the data form used for gathering data. + */ + private Form(DataForm dataForm) { + this.dataForm = dataForm; + } + + /** + * Creates a new Form of a given type from scratch.<p> + * + * Possible form types are: + * <ul> + * <li>form -> Indicates a form to fill out.</li> + * <li>submit -> The form is filled out, and this is the data that is being returned from + * the form.</li> + * <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li> + * <li>result -> Data results being returned from a search, or some other query.</li> + * </ul> + * + * @param type the form's type (e.g. form, submit,cancel,result). + */ + public Form(String type) { + this.dataForm = new DataForm(type); + } + + /** + * Adds a new field to complete as part of the form. + * + * @param field the field to complete. + */ + public void addField(FormField field) { + dataForm.addField(field); + } + + /** + * Sets a new String value to a given form's field. The field whose variable matches the + * requested variable will be completed with the specified value. If no field could be found + * for the specified variable then an exception will be raised.<p> + * + * If the value to set to the field is not a basic type (e.g. String, boolean, int, etc.) you + * can use this message where the String value is the String representation of the object. + * + * @param variable the variable name that was completed. + * @param value the String value that was answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable. + * @throws IllegalArgumentException if the answer type does not correspond with the field type. + */ + public void setAnswer(String variable, String value) { + FormField field = getField(variable); + if (field == null) { + throw new IllegalArgumentException("Field not found for the specified variable name."); + } + if (!FormField.TYPE_TEXT_MULTI.equals(field.getType()) + && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType()) + && !FormField.TYPE_TEXT_SINGLE.equals(field.getType()) + && !FormField.TYPE_JID_SINGLE.equals(field.getType()) + && !FormField.TYPE_HIDDEN.equals(field.getType())) { + throw new IllegalArgumentException("This field is not of type String."); + } + setAnswer(field, value); + } + + /** + * Sets a new int value to a given form's field. The field whose variable matches the + * requested variable will be completed with the specified value. If no field could be found + * for the specified variable then an exception will be raised. + * + * @param variable the variable name that was completed. + * @param value the int value that was answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable. + * @throws IllegalArgumentException if the answer type does not correspond with the field type. + */ + public void setAnswer(String variable, int value) { + FormField field = getField(variable); + if (field == null) { + throw new IllegalArgumentException("Field not found for the specified variable name."); + } + if (!FormField.TYPE_TEXT_MULTI.equals(field.getType()) + && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType()) + && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) { + throw new IllegalArgumentException("This field is not of type int."); + } + setAnswer(field, new Integer(value)); + } + + /** + * Sets a new long value to a given form's field. The field whose variable matches the + * requested variable will be completed with the specified value. If no field could be found + * for the specified variable then an exception will be raised. + * + * @param variable the variable name that was completed. + * @param value the long value that was answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable. + * @throws IllegalArgumentException if the answer type does not correspond with the field type. + */ + public void setAnswer(String variable, long value) { + FormField field = getField(variable); + if (field == null) { + throw new IllegalArgumentException("Field not found for the specified variable name."); + } + if (!FormField.TYPE_TEXT_MULTI.equals(field.getType()) + && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType()) + && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) { + throw new IllegalArgumentException("This field is not of type long."); + } + setAnswer(field, new Long(value)); + } + + /** + * Sets a new float value to a given form's field. The field whose variable matches the + * requested variable will be completed with the specified value. If no field could be found + * for the specified variable then an exception will be raised. + * + * @param variable the variable name that was completed. + * @param value the float value that was answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable. + * @throws IllegalArgumentException if the answer type does not correspond with the field type. + */ + public void setAnswer(String variable, float value) { + FormField field = getField(variable); + if (field == null) { + throw new IllegalArgumentException("Field not found for the specified variable name."); + } + if (!FormField.TYPE_TEXT_MULTI.equals(field.getType()) + && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType()) + && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) { + throw new IllegalArgumentException("This field is not of type float."); + } + setAnswer(field, new Float(value)); + } + + /** + * Sets a new double value to a given form's field. The field whose variable matches the + * requested variable will be completed with the specified value. If no field could be found + * for the specified variable then an exception will be raised. + * + * @param variable the variable name that was completed. + * @param value the double value that was answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable. + * @throws IllegalArgumentException if the answer type does not correspond with the field type. + */ + public void setAnswer(String variable, double value) { + FormField field = getField(variable); + if (field == null) { + throw new IllegalArgumentException("Field not found for the specified variable name."); + } + if (!FormField.TYPE_TEXT_MULTI.equals(field.getType()) + && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType()) + && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) { + throw new IllegalArgumentException("This field is not of type double."); + } + setAnswer(field, new Double(value)); + } + + /** + * Sets a new boolean value to a given form's field. The field whose variable matches the + * requested variable will be completed with the specified value. If no field could be found + * for the specified variable then an exception will be raised. + * + * @param variable the variable name that was completed. + * @param value the boolean value that was answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable. + * @throws IllegalArgumentException if the answer type does not correspond with the field type. + */ + public void setAnswer(String variable, boolean value) { + FormField field = getField(variable); + if (field == null) { + throw new IllegalArgumentException("Field not found for the specified variable name."); + } + if (!FormField.TYPE_BOOLEAN.equals(field.getType())) { + throw new IllegalArgumentException("This field is not of type boolean."); + } + setAnswer(field, (value ? "1" : "0")); + } + + /** + * Sets a new Object value to a given form's field. In fact, the object representation + * (i.e. #toString) will be the actual value of the field.<p> + * + * If the value to set to the field is not a basic type (e.g. String, boolean, int, etc.) you + * will need to use {@link #setAnswer(String, String))} where the String value is the + * String representation of the object.<p> + * + * Before setting the new value to the field we will check if the form is of type submit. If + * the form isn't of type submit means that it's not possible to complete the form and an + * exception will be thrown. + * + * @param field the form field that was completed. + * @param value the Object value that was answered. The object representation will be the + * actual value. + * @throws IllegalStateException if the form is not of type "submit". + */ + private void setAnswer(FormField field, Object value) { + if (!isSubmitType()) { + throw new IllegalStateException("Cannot set an answer if the form is not of type " + + "\"submit\""); + } + field.resetValues(); + field.addValue(value.toString()); + } + + /** + * Sets a new values to a given form's field. The field whose variable matches the requested + * variable will be completed with the specified values. If no field could be found for + * the specified variable then an exception will be raised.<p> + * + * The Objects contained in the List could be of any type. The String representation of them + * (i.e. #toString) will be actually used when sending the answer to the server. + * + * @param variable the variable that was completed. + * @param values the values that were answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable. + */ + public void setAnswer(String variable, List values) { + if (!isSubmitType()) { + throw new IllegalStateException("Cannot set an answer if the form is not of type " + + "\"submit\""); + } + FormField field = getField(variable); + if (field != null) { + // Check that the field can accept a collection of values + if (!FormField.TYPE_JID_MULTI.equals(field.getType()) + && !FormField.TYPE_LIST_MULTI.equals(field.getType()) + && !FormField.TYPE_LIST_SINGLE.equals(field.getType()) + && !FormField.TYPE_HIDDEN.equals(field.getType())) { + throw new IllegalArgumentException("This field only accept list of values."); + } + // Clear the old values + field.resetValues(); + // Set the new values. The string representation of each value will be actually used. + field.addValues(values); + } + else { + throw new IllegalArgumentException("Couldn't find a field for the specified variable."); + } + } + + /** + * Sets the default value as the value of a given form's field. The field whose variable matches + * the requested variable will be completed with its default value. If no field could be found + * for the specified variable then an exception will be raised. + * + * @param variable the variable to complete with its default value. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable. + */ + public void setDefaultAnswer(String variable) { + if (!isSubmitType()) { + throw new IllegalStateException("Cannot set an answer if the form is not of type " + + "\"submit\""); + } + FormField field = getField(variable); + if (field != null) { + // Clear the old values + field.resetValues(); + // Set the default value + for (Iterator it = field.getValues(); it.hasNext();) { + field.addValue((String) it.next()); + } + } + else { + throw new IllegalArgumentException("Couldn't find a field for the specified variable."); + } + } + + /** + * Returns an Iterator for the fields that are part of the form. + * + * @return an Iterator for the fields that are part of the form. + */ + public Iterator getFields() { + return dataForm.getFields(); + } + + /** + * Returns the field of the form whose variable matches the specified variable. + * The fields of type FIXED will never be returned since they do not specify a + * variable. + * + * @param variable the variable to look for in the form fields. + * @return the field of the form whose variable matches the specified variable. + */ + public FormField getField(String variable) { + if (variable == null || variable.equals("")) { + throw new IllegalArgumentException("Variable must not be null or blank."); + } + // Look for the field whose variable matches the requested variable + FormField field; + for (Iterator it=getFields();it.hasNext();) { + field = (FormField)it.next(); + if (variable.equals(field.getVariable())) { + return field; + } + } + return null; + } + + /** + * Returns the instructions that explain how to fill out the form and what the form is about. + * + * @return instructions that explain how to fill out the form. + */ + public String getInstructions() { + StringBuffer sb = new StringBuffer(); + // Join the list of instructions together separated by newlines + for (Iterator it = dataForm.getInstructions(); it.hasNext();) { + sb.append((String) it.next()); + // If this is not the last instruction then append a newline + if (it.hasNext()) { + sb.append("\n"); + } + } + return sb.toString(); + } + + + /** + * Returns the description of the data. It is similar to the title on a web page or an X + * window. You can put a <title/> on either a form to fill out, or a set of data results. + * + * @return description of the data. + */ + public String getTitle() { + return dataForm.getTitle(); + } + + + /** + * Returns the meaning of the data within the context. The data could be part of a form + * to fill out, a form submission or data results.<p> + * + * Possible form types are: + * <ul> + * <li>form -> Indicates a form to fill out.</li> + * <li>submit -> The form is filled out, and this is the data that is being returned from + * the form.</li> + * <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li> + * <li>result -> Data results being returned from a search, or some other query.</li> + * </ul> + * + * @return the form's type. + */ + public String getType() { + return dataForm.getType(); + } + + + /** + * Sets instructions that explain how to fill out the form and what the form is about. + * + * @param instructions instructions that explain how to fill out the form. + */ + public void setInstructions(String instructions) { + // Split the instructions into multiple instructions for each existent newline + ArrayList instructionsList = new ArrayList(); + StringTokenizer st = new StringTokenizer(instructions, "\n"); + while (st.hasMoreTokens()) { + instructionsList.add(st.nextToken()); + } + // Set the new list of instructions + dataForm.setInstructions(instructionsList); + + } + + + /** + * Sets the description of the data. It is similar to the title on a web page or an X window. + * You can put a <title/> on either a form to fill out, or a set of data results. + * + * @param title description of the data. + */ + public void setTitle(String title) { + dataForm.setTitle(title); + } + + /** + * Returns a DataForm that serves to send this Form to the server. If the form is of type + * submit, it may contain fields with no value. These fields will be removed since they only + * exist to assist the user while editing/completing the form in a UI. + * + * @return the wrapped DataForm. + */ + public DataForm getDataFormToSend() { + if (isSubmitType()) { + // Create a new DataForm that contains only the answered fields + DataForm dataFormToSend = new DataForm(getType()); + for(Iterator it=getFields();it.hasNext();) { + FormField field = (FormField)it.next(); + if (field.getValues().hasNext()) { + dataFormToSend.addField(field); + } + } + return dataFormToSend; + } + return dataForm; + } + + /** + * Returns true if the form is a form to fill out. + * + * @return if the form is a form to fill out. + */ + private boolean isFormType() { + return TYPE_FORM.equals(dataForm.getType()); + } + + /** + * Returns true if the form is a form to submit. + * + * @return if the form is a form to submit. + */ + private boolean isSubmitType() { + return TYPE_SUBMIT.equals(dataForm.getType()); + } + + /** + * Returns a new Form to submit the completed values. The new Form will include all the fields + * of the original form except for the fields of type FIXED. Only the HIDDEN fields will + * include the same value of the original form. The other fields of the new form MUST be + * completed. If a field remains with no answer when sending the completed form, then it won't + * be included as part of the completed form.<p> + * + * The reason why the fields with variables are included in the new form is to provide a model + * for binding with any UI. This means that the UIs will use the original form (of type + * "form") to learn how to render the form, but the UIs will bind the fields to the form of + * type submit. + * + * @return a Form to submit the completed values. + */ + public Form createAnswerForm() { + if (!isFormType()) { + throw new IllegalStateException("Only forms of type \"form\" could be answered"); + } + // Create a new Form + Form form = new Form(TYPE_SUBMIT); + for (Iterator fields=getFields(); fields.hasNext();) { + FormField field = (FormField)fields.next(); + // Add to the new form any type of field that includes a variable. + // Note: The fields of type FIXED are the only ones that don't specify a variable + if (field.getVariable() != null) { + FormField newField = new FormField(field.getVariable()); + newField.setType(field.getType()); + form.addField(newField); + // Set the answer ONLY to the hidden fields + if (FormField.TYPE_HIDDEN.equals(field.getType())) { + // Since a hidden field could have many values we need to collect them + // in a list + List values = new ArrayList(); + for (Iterator it=field.getValues();it.hasNext();) { + values.add((String)it.next()); + } + form.setAnswer(field.getVariable(), values); + } + } + } + return form; + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/FormField.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/FormField.java new file mode 100644 index 000000000..c8fafb965 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/FormField.java @@ -0,0 +1,354 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import java.util.*; + +/** + * Represents a field of a form. The field could be used to represent a question to complete, + * a completed question or a data returned from a search. The exact interpretation of the field + * depends on the context where the field is used. + * + * @author Gaston Dombiak + */ +public class FormField { + public static final String TYPE_BOOLEAN = "boolean"; + public static final String TYPE_FIXED = "fixed"; + public static final String TYPE_HIDDEN = "hidden"; + public static final String TYPE_JID_MULTI = "jid-multi"; + public static final String TYPE_JID_SINGLE = "jid-single"; + public static final String TYPE_LIST_MULTI = "list-multi"; + public static final String TYPE_LIST_SINGLE = "list-single"; + public static final String TYPE_TEXT_MULTI = "text-multi"; + public static final String TYPE_TEXT_PRIVATE = "text-private"; + public static final String TYPE_TEXT_SINGLE = "text-single"; + + private String description; + private boolean required = false; + private String label; + private String variable; + private String type; + private List options = new ArrayList(); + private List values = new ArrayList(); + + /** + * Creates a new FormField with the variable name that uniquely identifies the field + * in the context of the form. + * + * @param variable the variable name of the question. + */ + public FormField(String variable) { + this.variable = variable; + } + + /** + * Creates a new FormField of type FIXED. The fields of type FIXED do not define a variable + * name. + * + */ + public FormField() { + this.type = FormField.TYPE_FIXED; + } + + /** + * Returns a description that provides extra clarification about the question. This information + * could be presented to the user either in tool-tip, help button, or as a section of text + * before the question.<p> + * + * If the question is of type FIXED then the description should remain empty. + * + * @return description that provides extra clarification about the question. + */ + public String getDescription() { + return description; + } + + /** + * Returns the label of the question which should give enough information to the user to + * fill out the form. + * + * @return label of the question. + */ + public String getLabel() { + return label; + } + + /** + * Returns an Iterator for the available options that the user has in order to answer + * the question. + * + * @return Iterator for the available options. + */ + public Iterator getOptions() { + synchronized (options) { + return Collections.unmodifiableList(new ArrayList(options)).iterator(); + } + } + + /** + * Returns true if the question must be answered in order to complete the questionnaire. + * + * @return true if the question must be answered in order to complete the questionnaire. + */ + public boolean isRequired() { + return required; + } + + /** + * Returns an indicative of the format for the data to answer. Valid formats are: + * + * <ul> + * <li>text-single -> single line or word of text + * <li>text-private -> instead of showing the user what they typed, you show ***** to + * protect it + * <li>text-multi -> multiple lines of text entry + * <li>list-single -> given a list of choices, pick one + * <li>list-multi -> given a list of choices, pick one or more + * <li>boolean -> 0 or 1, true or false, yes or no. Default value is 0 + * <li>fixed -> fixed for putting in text to show sections, or just advertise your web + * site in the middle of the form + * <li>hidden -> is not given to the user at all, but returned with the questionnaire + * <li>jid-single -> Jabber ID - choosing a JID from your roster, and entering one based + * on the rules for a JID. + * <li>jid-multi -> multiple entries for JIDs + * </ul> + * + * @return format for the data to answer. + */ + public String getType() { + return type; + } + + /** + * Returns an Iterator for the default values of the question if the question is part + * of a form to fill out. Otherwise, returns an Iterator for the answered values of + * the question. + * + * @return an Iterator for the default values or answered values of the question. + */ + public Iterator getValues() { + synchronized (values) { + return Collections.unmodifiableList(new ArrayList(values)).iterator(); + } + } + + /** + * Returns the variable name that the question is filling out. + * + * @return the variable name of the question. + */ + public String getVariable() { + return variable; + } + + /** + * Sets a description that provides extra clarification about the question. This information + * could be presented to the user either in tool-tip, help button, or as a section of text + * before the question.<p> + * + * If the question is of type FIXED then the description should remain empty. + * + * @param description provides extra clarification about the question. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Sets the label of the question which should give enough information to the user to + * fill out the form. + * + * @param label the label of the question. + */ + public void setLabel(String label) { + this.label = label; + } + + /** + * Sets if the question must be answered in order to complete the questionnaire. + * + * @param required if the question must be answered in order to complete the questionnaire. + */ + public void setRequired(boolean required) { + this.required = required; + } + + /** + * Sets an indicative of the format for the data to answer. Valid formats are: + * + * <ul> + * <li>text-single -> single line or word of text + * <li>text-private -> instead of showing the user what they typed, you show ***** to + * protect it + * <li>text-multi -> multiple lines of text entry + * <li>list-single -> given a list of choices, pick one + * <li>list-multi -> given a list of choices, pick one or more + * <li>boolean -> 0 or 1, true or false, yes or no. Default value is 0 + * <li>fixed -> fixed for putting in text to show sections, or just advertise your web + * site in the middle of the form + * <li>hidden -> is not given to the user at all, but returned with the questionnaire + * <li>jid-single -> Jabber ID - choosing a JID from your roster, and entering one based + * on the rules for a JID. + * <li>jid-multi -> multiple entries for JIDs + * </ul> + * + * @param type an indicative of the format for the data to answer. + */ + public void setType(String type) { + this.type = type; + } + + /** + * Adds a default value to the question if the question is part of a form to fill out. + * Otherwise, adds an answered value to the question. + * + * @param value a default value or an answered value of the question. + */ + public void addValue(String value) { + synchronized (values) { + values.add(value); + } + } + + /** + * Adds a default values to the question if the question is part of a form to fill out. + * Otherwise, adds an answered values to the question. + * + * @param newValues default values or an answered values of the question. + */ + public void addValues(List newValues) { + synchronized (values) { + values.addAll(newValues); + } + } + + /** + * Removes all the values of the field. + * + */ + protected void resetValues() { + synchronized (values) { + values.removeAll(new ArrayList(values)); + } + } + + /** + * Adss an available options to the question that the user has in order to answer + * the question. + * + * @param option a new available option for the question. + */ + public void addOption(Option option) { + synchronized (options) { + options.add(option); + } + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<field"); + // Add attributes + if (getLabel() != null) { + buf.append(" label=\"").append(getLabel()).append("\""); + } + if (getVariable() != null) { + buf.append(" var=\"").append(getVariable()).append("\""); + } + if (getType() != null) { + buf.append(" type=\"").append(getType()).append("\""); + } + buf.append(">"); + // Add elements + if (getDescription() != null) { + buf.append("<desc>").append(getDescription()).append("</desc>"); + } + if (isRequired()) { + buf.append("<required/>"); + } + // Loop through all the values and append them to the string buffer + for (Iterator i = getValues(); i.hasNext();) { + buf.append("<value>").append(i.next()).append("</value>"); + } + // Loop through all the values and append them to the string buffer + for (Iterator i = getOptions(); i.hasNext();) { + buf.append(((Option)i.next()).toXML()); + } + buf.append("</field>"); + return buf.toString(); + } + + /** + * + * Represents the available option of a given FormField. + * + * @author Gaston Dombiak + */ + public static class Option { + private String label; + private String value; + + public Option(String value) { + this.value = value; + } + + public Option(String label, String value) { + this.label = label; + this.value = value; + } + + /** + * Returns the label that represents the option. + * + * @return the label that represents the option. + */ + public String getLabel() { + return label; + } + + /** + * Returns the value of the option. + * + * @return the value of the option. + */ + public String getValue() { + return value; + } + + public String toString(){ + return getLabel(); + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<option"); + // Add attribute + if (getLabel() != null) { + buf.append(" label=\"").append(getLabel()).append("\""); + } + buf.append(">"); + // Add element + buf.append("<value>").append(getValue()).append("</value>"); + + buf.append("</option>"); + return buf.toString(); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/GroupChatInvitation.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/GroupChatInvitation.java new file mode 100644 index 000000000..c2ee81ad1 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/GroupChatInvitation.java @@ -0,0 +1,115 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.xmlpull.v1.XmlPullParser; + +/** + * A group chat invitation packet extension, which is used to invite other + * users to a group chat room. To invite a user to a group chat room, address + * a new message to the user and set the room name appropriately, as in the + * following code example: + * + * <pre> + * Message message = new Message("user@chat.example.com"); + * message.setBody("Join me for a group chat!"); + * message.addExtension(new GroupChatInvitation("room@chat.example.com");); + * con.sendPacket(message); + * </pre> + * + * To listen for group chat invitations, use a PacketExtensionFilter for the + * <tt>x</tt> element name and <tt>jabber:x:conference</tt> namespace, as in the + * following code example: + * + * <pre> + * PacketFilter filter = new PacketExtensionFilter("x", "jabber:x:conference"); + * // Create a packet collector or packet listeners using the filter... + * </pre> + * + * <b>Note</b>: this protocol is outdated now that the Multi-User Chat (MUC) JEP is available + * (<a href="http://www.jabber.org/jeps/jep-0045.html">JEP-45</a>). However, most + * existing clients still use this older protocol. Once MUC support becomes more + * widespread, this API may be deprecated. + * + * @author Matt Tucker + */ +public class GroupChatInvitation implements PacketExtension { + + /** + * Element name of the packet extension. + */ + public static final String ELEMENT_NAME = "x"; + + /** + * Namespace of the packet extension. + */ + public static final String NAMESPACE = "jabber:x:conference"; + + private String roomAddress; + + /** + * Creates a new group chat invitation to the specified room address. + * GroupChat room addresses are in the form <tt>room@service</tt>, + * where <tt>service</tt> is the name of groupchat server, such as + * <tt>chat.example.com</tt>. + * + * @param roomAddress the address of the group chat room. + */ + public GroupChatInvitation(String roomAddress) { + this.roomAddress = roomAddress; + } + + /** + * Returns the address of the group chat room. GroupChat room addresses + * are in the form <tt>room@service</tt>, where <tt>service</tt> is + * the name of groupchat server, such as <tt>chat.example.com</tt>. + * + * @return the address of the group chat room. + */ + public String getRoomAddress() { + return roomAddress; + } + + public String getElementName() { + return ELEMENT_NAME; + } + + public String getNamespace() { + return NAMESPACE; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<x xmlns=\"jabber:x:conference\" jid=\"").append(roomAddress).append("\"/>"); + return buf.toString(); + } + + public static class Provider implements PacketExtensionProvider { + public PacketExtension parseExtension (XmlPullParser parser) throws Exception { + String roomAddress = parser.getAttributeValue("", "jid"); + // Advance to end of extension. + parser.next(); + return new GroupChatInvitation(roomAddress); + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventManager.java new file mode 100644 index 000000000..3fd6c6859 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventManager.java @@ -0,0 +1,304 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.filter.*; +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smackx.packet.*; + +/** + * Manages message events requests and notifications. A MessageEventManager provides a high + * level access to request for notifications and send event notifications. It also provides + * an easy way to hook up custom logic when requests or notifications are received. + * + * @author Gaston Dombiak + */ +public class MessageEventManager { + + private List messageEventNotificationListeners = new ArrayList(); + private List messageEventRequestListeners = new ArrayList(); + + private XMPPConnection con; + + private PacketFilter packetFilter = new PacketExtensionFilter("x", "jabber:x:event"); + private PacketListener packetListener; + + /** + * Creates a new message event manager. + * + * @param con an XMPPConnection. + */ + public MessageEventManager(XMPPConnection con) { + this.con = con; + init(); + } + + /** + * Adds event notification requests to a message. For each event type that + * the user wishes event notifications from the message recepient for, <tt>true</tt> + * should be passed in to this method. + * + * @param message the message to add the requested notifications. + * @param offline specifies if the offline event is requested. + * @param delivered specifies if the delivered event is requested. + * @param displayed specifies if the displayed event is requested. + * @param composing specifies if the composing event is requested. + */ + public static void addNotificationsRequests(Message message, boolean offline, + boolean delivered, boolean displayed, boolean composing) + { + // Create a MessageEvent Package and add it to the message + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setOffline(offline); + messageEvent.setDelivered(delivered); + messageEvent.setDisplayed(displayed); + messageEvent.setComposing(composing); + message.addExtension(messageEvent); + } + + /** + * Adds a message event request listener. The listener will be fired anytime a request for + * event notification is received. + * + * @param messageEventRequestListener a message event request listener. + */ + public void addMessageEventRequestListener(MessageEventRequestListener messageEventRequestListener) { + synchronized (messageEventRequestListeners) { + if (!messageEventRequestListeners.contains(messageEventRequestListener)) { + messageEventRequestListeners.add(messageEventRequestListener); + } + } + } + + /** + * Removes a message event request listener. The listener will be fired anytime a request for + * event notification is received. + * + * @param messageEventRequestListener a message event request listener. + */ + public void removeMessageEventRequestListener(MessageEventRequestListener messageEventRequestListener) { + synchronized (messageEventRequestListeners) { + messageEventRequestListeners.remove(messageEventRequestListener); + } + } + + /** + * Adds a message event notification listener. The listener will be fired anytime a notification + * event is received. + * + * @param messageEventNotificationListener a message event notification listener. + */ + public void addMessageEventNotificationListener(MessageEventNotificationListener messageEventNotificationListener) { + synchronized (messageEventNotificationListeners) { + if (!messageEventNotificationListeners.contains(messageEventNotificationListener)) { + messageEventNotificationListeners.add(messageEventNotificationListener); + } + } + } + + /** + * Removes a message event notification listener. The listener will be fired anytime a notification + * event is received. + * + * @param messageEventNotificationListener a message event notification listener. + */ + public void removeMessageEventNotificationListener(MessageEventNotificationListener messageEventNotificationListener) { + synchronized (messageEventNotificationListeners) { + messageEventNotificationListeners.remove(messageEventNotificationListener); + } + } + + /** + * Fires message event request listeners. + */ + private void fireMessageEventRequestListeners( + String from, + String packetID, + String methodName) { + MessageEventRequestListener[] listeners = null; + Method method; + synchronized (messageEventRequestListeners) { + listeners = new MessageEventRequestListener[messageEventRequestListeners.size()]; + messageEventRequestListeners.toArray(listeners); + } + try { + method = + MessageEventRequestListener.class.getDeclaredMethod( + methodName, + new Class[] { String.class, String.class, MessageEventManager.class }); + for (int i = 0; i < listeners.length; i++) { + method.invoke(listeners[i], new Object[] { from, packetID, this }); + } + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + /** + * Fires message event notification listeners. + */ + private void fireMessageEventNotificationListeners( + String from, + String packetID, + String methodName) { + MessageEventNotificationListener[] listeners = null; + Method method; + synchronized (messageEventNotificationListeners) { + listeners = + new MessageEventNotificationListener[messageEventNotificationListeners.size()]; + messageEventNotificationListeners.toArray(listeners); + } + try { + method = + MessageEventNotificationListener.class.getDeclaredMethod( + methodName, + new Class[] { String.class, String.class }); + for (int i = 0; i < listeners.length; i++) { + method.invoke(listeners[i], new Object[] { from, packetID }); + } + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + private void init() { + // Listens for all message event packets and fire the proper message event listeners. + packetListener = new PacketListener() { + public void processPacket(Packet packet) { + Message message = (Message) packet; + MessageEvent messageEvent = + (MessageEvent) message.getExtension("x", "jabber:x:event"); + if (messageEvent.isMessageEventRequest()) { + // Fire event for requests of message events + for (Iterator it = messageEvent.getEventTypes(); it.hasNext();) + fireMessageEventRequestListeners( + message.getFrom(), + message.getPacketID(), + ((String) it.next()).concat("NotificationRequested")); + } else + // Fire event for notifications of message events + for (Iterator it = messageEvent.getEventTypes(); it.hasNext();) + fireMessageEventNotificationListeners( + message.getFrom(), + messageEvent.getPacketID(), + ((String) it.next()).concat("Notification")); + + }; + + }; + con.addPacketListener(packetListener, packetFilter); + } + + /** + * Sends the notification that the message was delivered to the sender of the original message + * + * @param to the recipient of the notification. + * @param packetID the id of the message to send. + */ + public void sendDeliveredNotification(String to, String packetID) { + // Create the message to send + Message msg = new Message(to); + // Create a MessageEvent Package and add it to the message + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setDelivered(true); + messageEvent.setPacketID(packetID); + msg.addExtension(messageEvent); + // Send the packet + con.sendPacket(msg); + } + + /** + * Sends the notification that the message was displayed to the sender of the original message + * + * @param to the recipient of the notification. + * @param packetID the id of the message to send. + */ + public void sendDisplayedNotification(String to, String packetID) { + // Create the message to send + Message msg = new Message(to); + // Create a MessageEvent Package and add it to the message + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setDisplayed(true); + messageEvent.setPacketID(packetID); + msg.addExtension(messageEvent); + // Send the packet + con.sendPacket(msg); + } + + /** + * Sends the notification that the receiver of the message is composing a reply + * + * @param to the recipient of the notification. + * @param packetID the id of the message to send. + */ + public void sendComposingNotification(String to, String packetID) { + // Create the message to send + Message msg = new Message(to); + // Create a MessageEvent Package and add it to the message + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setComposing(true); + messageEvent.setPacketID(packetID); + msg.addExtension(messageEvent); + // Send the packet + con.sendPacket(msg); + } + + /** + * Sends the notification that the receiver of the message has cancelled composing a reply. + * + * @param to the recipient of the notification. + * @param packetID the id of the message to send. + */ + public void sendCancelledNotification(String to, String packetID) { + // Create the message to send + Message msg = new Message(to); + // Create a MessageEvent Package and add it to the message + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setCancelled(true); + messageEvent.setPacketID(packetID); + msg.addExtension(messageEvent); + // Send the packet + con.sendPacket(msg); + } + + public void destroy() { + if (con != null) { + con.removePacketListener(packetListener); + } + } + + public void finalize() { + destroy(); + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventNotificationListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventNotificationListener.java new file mode 100644 index 000000000..e6442af40 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventNotificationListener.java @@ -0,0 +1,74 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +/** + * + * A listener that is fired anytime a message event notification is received. + * Message event notifications are received as a consequence of the request + * to receive notifications when sending a message. + * + * @author Gaston Dombiak + */ +public interface MessageEventNotificationListener { + + /** + * Called when a notification of message delivered is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + */ + public void deliveredNotification(String from, String packetID); + + /** + * Called when a notification of message displayed is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + */ + public void displayedNotification(String from, String packetID); + + /** + * Called when a notification that the receiver of the message is composing a reply is + * received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + */ + public void composingNotification(String from, String packetID); + + /** + * Called when a notification that the receiver of the message is offline is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + */ + public void offlineNotification(String from, String packetID); + + /** + * Called when a notification that the receiver of the message cancelled the reply + * is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + */ + public void cancelledNotification(String from, String packetID); +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventRequestListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventRequestListener.java new file mode 100644 index 000000000..107168ff4 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MessageEventRequestListener.java @@ -0,0 +1,86 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +/** + * + * A listener that is fired anytime a message event request is received. + * Message event requests are received when the received message includes an extension + * like this: + * + * <pre> + * <x xmlns='jabber:x:event'> + * <offline/> + * <delivered/> + * <composing/> + * </x> + * </pre> + * + * In this example you can see that the sender of the message requests to be notified + * when the user couldn't receive the message because he/she is offline, the message + * was delivered or when the receiver of the message is composing a reply. + * + * @author Gaston Dombiak + */ +public interface MessageEventRequestListener { + + /** + * Called when a request for message delivered notification is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + * @param messageEventManager the messageEventManager that fired the listener. + */ + public void deliveredNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager); + + /** + * Called when a request for message displayed notification is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + * @param messageEventManager the messageEventManager that fired the listener. + */ + public void displayedNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager); + + /** + * Called when a request that the receiver of the message is composing a reply notification is + * received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + * @param messageEventManager the messageEventManager that fired the listener. + */ + public void composingNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager); + + /** + * Called when a request that the receiver of the message is offline is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + * @param messageEventManager the messageEventManager that fired the listener. + */ + public void offlineNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager); + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientInfo.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientInfo.java new file mode 100644 index 000000000..c6aa004fd --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientInfo.java @@ -0,0 +1,98 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import org.jivesoftware.smackx.packet.MultipleAddresses; + +import java.util.List; + +/** + * MultipleRecipientInfo keeps information about the multiple recipients extension included + * in a received packet. Among the information we can find the list of TO and CC addresses. + * + * @author Gaston Dombiak + */ +public class MultipleRecipientInfo { + + MultipleAddresses extension; + + MultipleRecipientInfo(MultipleAddresses extension) { + this.extension = extension; + } + + /** + * Returns the list of {@link org.jivesoftware.smackx.packet.MultipleAddresses.Address} + * that were the primary recipients of the packet. + * + * @return list of primary recipients of the packet. + */ + public List getTOAddresses() { + return extension.getAddressesOfType(MultipleAddresses.TO); + } + + /** + * Returns the list of {@link org.jivesoftware.smackx.packet.MultipleAddresses.Address} + * that were the secondary recipients of the packet. + * + * @return list of secondary recipients of the packet. + */ + public List getCCAddresses() { + return extension.getAddressesOfType(MultipleAddresses.CC); + } + + /** + * Returns the JID of a MUC room to which responses should be sent or <tt>null</tt> if + * no specific address was provided. When no specific address was provided then the reply + * can be sent to any or all recipients. Otherwise, the user should join the specified room + * and send the reply to the room. + * + * @return the JID of a MUC room to which responses should be sent or <tt>null</tt> if + * no specific address was provided. + */ + public String getReplyRoom() { + List replyRoom = extension.getAddressesOfType(MultipleAddresses.REPLY_ROOM); + return replyRoom.isEmpty() ? null : ((MultipleAddresses.Address) replyRoom.get(0)).getJid(); + } + + /** + * Returns true if the received packet should not be replied. Use + * {@link MultipleRecipientManager#reply(org.jivesoftware.smack.XMPPConnection, org.jivesoftware.smack.packet.Message, org.jivesoftware.smack.packet.Message)} + * to send replies. + * + * @return true if the received packet should not be replied. + */ + public boolean shouldNotReply() { + return !extension.getAddressesOfType(MultipleAddresses.NO_REPLY).isEmpty(); + } + + /** + * Returns the address to which all replies are requested to be sent or <tt>null</tt> if + * no specific address was provided. When no specific address was provided then the reply + * can be sent to any or all recipients. + * + * @return the address to which all replies are requested to be sent or <tt>null</tt> if + * no specific address was provided. + */ + public MultipleAddresses.Address getReplyAddress() { + List replyTo = extension.getAddressesOfType(MultipleAddresses.REPLY_TO); + return replyTo.isEmpty() ? null : (MultipleAddresses.Address) replyTo.get(0); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientManager.java new file mode 100644 index 000000000..853836812 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/MultipleRecipientManager.java @@ -0,0 +1,353 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.util.Cache; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverItems; +import org.jivesoftware.smackx.packet.MultipleAddresses; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * A MultipleRecipientManager allows to send packets to multiple recipients by making use of + * <a href="http://www.jabber.org/jeps/jep-0033.html">JEP-33: Extended Stanza Addressing</a>. + * It also allows to send replies to packets that were sent to multiple recipients. + * + * @author Gaston Dombiak + */ +public class MultipleRecipientManager { + + /** + * Create a cache to hold the 100 most recently accessed elements for a period of + * 24 hours. + */ + private static Cache services = new Cache(100, 24 * 60 * 60 * 1000); + + /** + * Sends the specified packet to the list of specified recipients using the + * specified connection. If the server has support for JEP-33 then only one + * packet is going to be sent to the server with the multiple recipient instructions. + * However, if JEP-33 is not supported by the server then the client is going to send + * the packet to each recipient. + * + * @param connection the connection to use to send the packet. + * @param packet the packet to send to the list of recipients. + * @param to the list of JIDs to include in the TO list or <tt>null</tt> if no TO + * list exists. + * @param cc the list of JIDs to include in the CC list or <tt>null</tt> if no CC + * list exists. + * @param bcc the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC + * list exists. + * @throws XMPPException if server does not support JEP-33: Extended Stanza Addressing and + * some JEP-33 specific features were requested. + */ + public static void send(XMPPConnection connection, Packet packet, List to, List cc, List bcc) + throws XMPPException { + send(connection, packet, to, cc, bcc, null, null, false); + } + + /** + * Sends the specified packet to the list of specified recipients using the + * specified connection. If the server has support for JEP-33 then only one + * packet is going to be sent to the server with the multiple recipient instructions. + * However, if JEP-33 is not supported by the server then the client is going to send + * the packet to each recipient. + * + * @param connection the connection to use to send the packet. + * @param packet the packet to send to the list of recipients. + * @param to the list of JIDs to include in the TO list or <tt>null</tt> if no TO + * list exists. + * @param cc the list of JIDs to include in the CC list or <tt>null</tt> if no CC + * list exists. + * @param bcc the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC + * list exists. + * @param replyTo address to which all replies are requested to be sent or <tt>null</tt> + * indicating that they can reply to any address. + * @param replyRoom JID of a MUC room to which responses should be sent or <tt>null</tt> + * indicating that they can reply to any address. + * @param noReply true means that receivers should not reply to the message. + * @throws XMPPException if server does not support JEP-33: Extended Stanza Addressing and + * some JEP-33 specific features were requested. + */ + public static void send(XMPPConnection connection, Packet packet, List to, List cc, List bcc, + String replyTo, String replyRoom, boolean noReply) throws XMPPException { + String serviceAddress = getMultipleRecipienServiceAddress(connection); + if (serviceAddress != null) { + // Send packet to target users using multiple recipient service provided by the server + sendThroughService(connection, packet, to, cc, bcc, replyTo, replyRoom, noReply, + serviceAddress); + } + else { + // Server does not support JEP-33 so try to send the packet to each recipient + if (noReply || (replyTo != null && replyTo.trim().length() > 0) || + (replyRoom != null && replyRoom.trim().length() > 0)) { + // Some specified JEP-33 features were requested so throw an exception alerting + // the user that this features are not available + throw new XMPPException("Extended Stanza Addressing not supported by server"); + } + // Send the packet to each individual recipient + sendToIndividualRecipients(connection, packet, to, cc, bcc); + } + } + + /** + * Sends a reply to a previously received packet that was sent to multiple recipients. Before + * attempting to send the reply message some checkings are performed. If any of those checkings + * fail then an XMPPException is going to be thrown with the specific error detail. + * + * @param connection the connection to use to send the reply. + * @param original the previously received packet that was sent to multiple recipients. + * @param reply the new message to send as a reply. + * @throws XMPPException if the original message was not sent to multiple recipients, or the + * original message cannot be replied or reply should be sent to a room. + */ + public static void reply(XMPPConnection connection, Message original, Message reply) + throws XMPPException { + MultipleRecipientInfo info = getMultipleRecipientInfo(original); + if (info == null) { + throw new XMPPException("Original message does not contain multiple recipient info"); + } + if (info.shouldNotReply()) { + throw new XMPPException("Original message should not be replied"); + } + if (info.getReplyRoom() != null) { + throw new XMPPException("Reply should be sent through a room"); + } + // Any <thread/> element from the initial message MUST be copied into the reply. + if (original.getThread() != null) { + reply.setThread(original.getThread()); + } + MultipleAddresses.Address replyAddress = info.getReplyAddress(); + if (replyAddress != null && replyAddress.getJid() != null) { + // Send reply to the reply_to address + reply.setTo(replyAddress.getJid()); + connection.sendPacket(reply); + } + else { + // Send reply to multiple recipients + List to = new ArrayList(); + List cc = new ArrayList(); + for (Iterator it = info.getTOAddresses().iterator(); it.hasNext();) { + String jid = ((MultipleAddresses.Address) it.next()).getJid(); + to.add(jid); + } + for (Iterator it = info.getCCAddresses().iterator(); it.hasNext();) { + String jid = ((MultipleAddresses.Address) it.next()).getJid(); + cc.add(jid); + } + // Add original sender as a 'to' address (if not already present) + if (!to.contains(original.getFrom()) && !cc.contains(original.getFrom())) { + to.add(original.getFrom()); + } + // Remove the sender from the TO/CC list (try with bare JID too) + String from = connection.getUser(); + if (!to.remove(from) && !cc.remove(from)) { + String bareJID = StringUtils.parseBareAddress(from); + to.remove(bareJID); + cc.remove(bareJID); + } + + String serviceAddress = getMultipleRecipienServiceAddress(connection); + if (serviceAddress != null) { + // Send packet to target users using multiple recipient service provided by the server + sendThroughService(connection, reply, to, cc, null, null, null, false, + serviceAddress); + } + else { + // Server does not support JEP-33 so try to send the packet to each recipient + sendToIndividualRecipients(connection, reply, to, cc, null); + } + } + } + + /** + * Returns the {@link MultipleRecipientInfo} contained in the specified packet or + * <tt>null</tt> if none was found. Only packets sent to multiple recipients will + * contain such information. + * + * @param packet the packet to check. + * @return the MultipleRecipientInfo contained in the specified packet or <tt>null</tt> + * if none was found. + */ + public static MultipleRecipientInfo getMultipleRecipientInfo(Packet packet) { + MultipleAddresses extension = (MultipleAddresses) packet + .getExtension("addresses", "http://jabber.org/protocol/address"); + return extension == null ? null : new MultipleRecipientInfo(extension); + } + + private static void sendToIndividualRecipients(XMPPConnection connection, Packet packet, + List to, List cc, List bcc) { + if (to != null) { + for (Iterator it = to.iterator(); it.hasNext();) { + String jid = (String) it.next(); + packet.setTo(jid); + connection.sendPacket(new PacketCopy(packet.toXML())); + } + } + if (cc != null) { + for (Iterator it = cc.iterator(); it.hasNext();) { + String jid = (String) it.next(); + packet.setTo(jid); + connection.sendPacket(new PacketCopy(packet.toXML())); + } + } + if (bcc != null) { + for (Iterator it = bcc.iterator(); it.hasNext();) { + String jid = (String) it.next(); + packet.setTo(jid); + connection.sendPacket(new PacketCopy(packet.toXML())); + } + } + } + + private static void sendThroughService(XMPPConnection connection, Packet packet, List to, + List cc, List bcc, String replyTo, String replyRoom, boolean noReply, + String serviceAddress) { + // Create multiple recipient extension + MultipleAddresses multipleAddresses = new MultipleAddresses(); + if (to != null) { + for (Iterator it = to.iterator(); it.hasNext();) { + String jid = (String) it.next(); + multipleAddresses.addAddress(MultipleAddresses.TO, jid, null, null, false, null); + } + } + if (cc != null) { + for (Iterator it = cc.iterator(); it.hasNext();) { + String jid = (String) it.next(); + multipleAddresses.addAddress(MultipleAddresses.CC, jid, null, null, false, null); + } + } + if (bcc != null) { + for (Iterator it = bcc.iterator(); it.hasNext();) { + String jid = (String) it.next(); + multipleAddresses.addAddress(MultipleAddresses.BCC, jid, null, null, false, null); + } + } + if (noReply) { + multipleAddresses.setNoReply(); + } + else { + if (replyTo != null && replyTo.trim().length() > 0) { + multipleAddresses + .addAddress(MultipleAddresses.REPLY_TO, replyTo, null, null, false, null); + } + if (replyRoom != null && replyRoom.trim().length() > 0) { + multipleAddresses.addAddress(MultipleAddresses.REPLY_ROOM, replyRoom, null, null, + false, null); + } + } + // Set the multiple recipient service address as the target address + packet.setTo(serviceAddress); + // Add extension to packet + packet.addExtension(multipleAddresses); + // Send the packet + connection.sendPacket(packet); + } + + /** + * Returns the address of the multiple recipients service. To obtain such address service + * discovery is going to be used on the connected server and if none was found then another + * attempt will be tried on the server items. The discovered information is going to be + * cached for 24 hours. + * + * @param connection the connection to use for disco. The connected server is going to be + * queried. + * @return the address of the multiple recipients service or <tt>null</tt> if none was found. + */ + private static String getMultipleRecipienServiceAddress(XMPPConnection connection) { + String serviceName = connection.getServiceName(); + String serviceAddress = (String) services.get(serviceName); + if (serviceAddress == null) { + synchronized (services) { + serviceAddress = (String) services.get(serviceName); + if (serviceAddress == null) { + + // Send the disco packet to the server itself + try { + DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection) + .discoverInfo(serviceName); + // Check if the server supports JEP-33 + if (info.containsFeature("http://jabber.org/protocol/address")) { + serviceAddress = serviceName; + } + else { + // Get the disco items and send the disco packet to each server item + DiscoverItems items = ServiceDiscoveryManager.getInstanceFor(connection) + .discoverItems(serviceName); + for (Iterator it = items.getItems(); it.hasNext();) { + DiscoverItems.Item item = (DiscoverItems.Item) it.next(); + info = ServiceDiscoveryManager.getInstanceFor(connection) + .discoverInfo(item.getEntityID(), item.getNode()); + if (info.containsFeature("http://jabber.org/protocol/address")) { + serviceAddress = serviceName; + break; + } + } + + } + // Cache the discovered information + services.put(serviceName, serviceAddress == null ? "" : serviceAddress); + } + catch (XMPPException e) { + e.printStackTrace(); + } + } + } + } + + return "".equals(serviceAddress) ? null : serviceAddress; + } + + /** + * Packet that holds the XML stanza to send. This class is useful when the same packet + * is needed to be sent to different recipients. Since using the same packet is not possible + * (i.e. cannot change the TO address of a queues packet to be sent) then this class was + * created to keep the XML stanza to send. + */ + private static class PacketCopy extends Packet { + + private String text; + + /** + * Create a copy of a packet with the text to send. The passed text must be a valid text to + * send to the server, no validation will be done on the passed text. + * + * @param text the whole text of the packet to send + */ + public PacketCopy(String text) { + this.text = text; + } + + public String toXML() { + return text; + } + + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/NodeInformationProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/NodeInformationProvider.java new file mode 100644 index 000000000..2a06e2f8d --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/NodeInformationProvider.java @@ -0,0 +1,44 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import java.util.Iterator; + + +/** + * The NodeInformationProvider is responsible for providing information (i.e. DiscoverItems.Item) + * about a given node. This information will be requested each time this XMPPP client receives a + * disco items requests on the given node. + * + * @author Gaston Dombiak + */ +public interface NodeInformationProvider { + + /** + * Returns an Iterator on the Items {@link org.jivesoftware.smackx.packet.DiscoverItems.Item} + * defined in the node. For example, the MUC protocol specifies that an XMPP client should + * answer an Item for each joined room when asked for the rooms where the use has joined. + * + * @return an Iterator on the Items defined in the node. + */ + public abstract Iterator getNodeItems(); + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageHeader.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageHeader.java new file mode 100644 index 000000000..c3ff215a1 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageHeader.java @@ -0,0 +1,85 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import org.jivesoftware.smackx.packet.DiscoverItems; + +/** + * The OfflineMessageHeader holds header information of an offline message. The header + * information was retrieved using the {@link OfflineMessageManager} class.<p> + * + * Each offline message is identified by the target user of the offline message and a unique stamp. + * Use {@link OfflineMessageManager#getMessages(java.util.List)} to retrieve the whole message. + * + * @author Gaston Dombiak + */ +public class OfflineMessageHeader { + /** + * Bare JID of the user that was offline when the message was sent. + */ + private String user; + /** + * Full JID of the user that sent the message. + */ + private String jid; + /** + * Stamp that uniquely identifies the offline message. This stamp will be used for + * getting the specific message or delete it. The stamp may be of the form UTC timestamps + * but it is not required to have that format. + */ + private String stamp; + + public OfflineMessageHeader(DiscoverItems.Item item) { + super(); + user = item.getEntityID(); + jid = item.getName(); + stamp = item.getNode(); + } + + /** + * Returns the bare JID of the user that was offline when the message was sent. + * + * @return the bare JID of the user that was offline when the message was sent. + */ + public String getUser() { + return user; + } + + /** + * Returns the full JID of the user that sent the message. + * + * @return the full JID of the user that sent the message. + */ + public String getJid() { + return jid; + } + + /** + * Returns the stamp that uniquely identifies the offline message. This stamp will + * be used for getting the specific message or delete it. The stamp may be of the + * form UTC timestamps but it is not required to have that format. + * + * @return the stamp that uniquely identifies the offline message. + */ + public String getStamp() { + return stamp; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageManager.java new file mode 100644 index 000000000..ba4331d83 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/OfflineMessageManager.java @@ -0,0 +1,284 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.*; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverItems; +import org.jivesoftware.smackx.packet.OfflineMessageInfo; +import org.jivesoftware.smackx.packet.OfflineMessageRequest; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * The OfflineMessageManager helps manage offline messages even before the user has sent an + * available presence. When a user asks for his offline messages before sending an available + * presence then the server will not send a flood with all the offline messages when the user + * becomes online. The server will not send a flood with all the offline messages to the session + * that made the offline messages request or to any other session used by the user that becomes + * online.<p> + * + * Once the session that made the offline messages request has been closed and the user becomes + * offline in all the resources then the server will resume storing the messages offline and will + * send all the offline messages to the user when he becomes online. Therefore, the server will + * flood the user when he becomes online unless the user uses this class to manage his offline + * messages. + * + * @author Gaston Dombiak + */ +public class OfflineMessageManager { + + private final static String namespace = "http://jabber.org/protocol/offline"; + + private XMPPConnection connection; + + private PacketFilter packetFilter; + + public OfflineMessageManager(XMPPConnection connection) { + this.connection = connection; + packetFilter = + new AndFilter(new PacketExtensionFilter("offline", namespace), + new PacketTypeFilter(Message.class)); + } + + /** + * Returns true if the server supports Flexible Offline Message Retrieval. When the server + * supports Flexible Offline Message Retrieval it is possible to get the header of the offline + * messages, get specific messages, delete specific messages, etc. + * + * @return a boolean indicating if the server supports Flexible Offline Message Retrieval. + * @throws XMPPException If the user is not allowed to make this request. + */ + public boolean supportsFlexibleRetrieval() throws XMPPException { + DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(null); + return info.containsFeature(namespace); + } + + /** + * Returns the number of offline messages for the user of the connection. + * + * @return the number of offline messages for the user of the connection. + * @throws XMPPException If the user is not allowed to make this request or the server does + * not support offline message retrieval. + */ + public int getMessageCount() throws XMPPException { + DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(null, + namespace); + Form extendedInfo = Form.getFormFrom(info); + if (extendedInfo != null) { + String value = (String) extendedInfo.getField("number_of_messages").getValues().next(); + return Integer.parseInt(value); + } + return 0; + } + + /** + * Returns an iterator on <tt>OfflineMessageHeader</tt> that keep information about the + * offline message. The OfflineMessageHeader includes a stamp that could be used to retrieve + * the complete message or delete the specific message. + * + * @return an iterator on <tt>OfflineMessageHeader</tt> that keep information about the offline + * message. + * @throws XMPPException If the user is not allowed to make this request or the server does + * not support offline message retrieval. + */ + public Iterator getHeaders() throws XMPPException { + List answer = new ArrayList(); + DiscoverItems items = ServiceDiscoveryManager.getInstanceFor(connection).discoverItems( + null, namespace); + for (Iterator it = items.getItems(); it.hasNext();) { + DiscoverItems.Item item = (DiscoverItems.Item) it.next(); + answer.add(new OfflineMessageHeader(item)); + } + return answer.iterator(); + } + + /** + * Returns an Iterator with the offline <tt>Messages</tt> whose stamp matches the specified + * request. The request will include the list of stamps that uniquely identifies + * the offline messages to retrieve. The returned offline messages will not be deleted + * from the server. Use {@link #deleteMessages(java.util.List)} to delete the messages. + * + * @param nodes the list of stamps that uniquely identifies offline message. + * @return an Iterator with the offline <tt>Messages</tt> that were received as part of + * this request. + * @throws XMPPException If the user is not allowed to make this request or the server does + * not support offline message retrieval. + */ + public Iterator getMessages(final List nodes) throws XMPPException { + List messages = new ArrayList(); + OfflineMessageRequest request = new OfflineMessageRequest(); + for (Iterator it = nodes.iterator(); it.hasNext();) { + OfflineMessageRequest.Item item = new OfflineMessageRequest.Item((String) it.next()); + item.setAction("view"); + request.addItem(item); + } + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(request.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Filter offline messages that were requested by this request + PacketFilter messageFilter = new AndFilter(packetFilter, new PacketFilter() { + public boolean accept(Packet packet) { + OfflineMessageInfo info = (OfflineMessageInfo) packet.getExtension("offline", + namespace); + return nodes.contains(info.getNode()); + } + }); + PacketCollector messageCollector = connection.createPacketCollector(messageFilter); + // Send the retrieval request to the server. + connection.sendPacket(request); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + + // Collect the received offline messages + Message message = (Message) messageCollector.nextResult( + SmackConfiguration.getPacketReplyTimeout()); + while (message != null) { + messages.add(message); + message = + (Message) messageCollector.nextResult( + SmackConfiguration.getPacketReplyTimeout()); + } + // Stop queuing offline messages + messageCollector.cancel(); + return messages.iterator(); + } + + /** + * Returns an Iterator with all the offline <tt>Messages</tt> of the user. The returned offline + * messages will not be deleted from the server. Use {@link #deleteMessages(java.util.List)} + * to delete the messages. + * + * @return an Iterator with all the offline <tt>Messages</tt> of the user. + * @throws XMPPException If the user is not allowed to make this request or the server does + * not support offline message retrieval. + */ + public Iterator getMessages() throws XMPPException { + List messages = new ArrayList(); + OfflineMessageRequest request = new OfflineMessageRequest(); + request.setFetch(true); + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(request.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Filter offline messages that were requested by this request + PacketCollector messageCollector = connection.createPacketCollector(packetFilter); + // Send the retrieval request to the server. + connection.sendPacket(request); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + + // Collect the received offline messages + Message message = (Message) messageCollector.nextResult( + SmackConfiguration.getPacketReplyTimeout()); + while (message != null) { + messages.add(message); + message = + (Message) messageCollector.nextResult( + SmackConfiguration.getPacketReplyTimeout()); + } + // Stop queuing offline messages + messageCollector.cancel(); + return messages.iterator(); + } + + /** + * Deletes the specified list of offline messages. The request will include the list of + * stamps that uniquely identifies the offline messages to delete. + * + * @param nodes the list of stamps that uniquely identifies offline message. + * @throws XMPPException If the user is not allowed to make this request or the server does + * not support offline message retrieval. + */ + public void deleteMessages(List nodes) throws XMPPException { + OfflineMessageRequest request = new OfflineMessageRequest(); + for (Iterator it = nodes.iterator(); it.hasNext();) { + OfflineMessageRequest.Item item = new OfflineMessageRequest.Item((String) it.next()); + item.setAction("remove"); + request.addItem(item); + } + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(request.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the deletion request to the server. + connection.sendPacket(request); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + /** + * Deletes all offline messages of the user. + * + * @throws XMPPException If the user is not allowed to make this request or the server does + * not support offline message retrieval. + */ + public void deleteMessages() throws XMPPException { + OfflineMessageRequest request = new OfflineMessageRequest(); + request.setPurge(true); + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(request.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the deletion request to the server. + connection.sendPacket(request); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/PrivateDataManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/PrivateDataManager.java new file mode 100644 index 000000000..96bc6f781 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/PrivateDataManager.java @@ -0,0 +1,345 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.packet.*; +import org.jivesoftware.smackx.provider.*; +import org.xmlpull.v1.XmlPullParser; + +import java.util.Map; +import java.util.Hashtable; + +/** + * Manages private data, which is a mechanism to allow users to store arbitrary XML + * data on an XMPP server. Each private data chunk is defined by a element name and + * XML namespace. Example private data: + * + * <pre> + * <color xmlns="http://example.com/xmpp/color"> + * <favorite>blue</blue> + * <leastFavorite>puce</leastFavorite> + * </color> + * </pre> + * + * {@link PrivateDataProvider} instances are responsible for translating the XML into objects. + * If no PrivateDataProvider is registered for a given element name and namespace, then + * a {@link DefaultPrivateData} instance will be returned.<p> + * + * Warning: this is an non-standard protocol documented by + * <a href="http://www.jabber.org/jeps/jep-0049.html">JEP-49</a>. Because this is a + * non-standard protocol, it is subject to change. + * + * @author Matt Tucker + */ +public class PrivateDataManager { + + /** + * Map of provider instances. + */ + private static Map privateDataProviders = new Hashtable(); + + /** + * Returns the private data provider registered to the specified XML element name and namespace. + * For example, if a provider was registered to the element name "prefs" and the + * namespace "http://www.xmppclient.com/prefs", then the following packet would trigger + * the provider: + * + * <pre> + * <iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'> + * <query xmlns='jabber:iq:private'> + * <prefs xmlns='http://www.xmppclient.com/prefs'> + * <value1>ABC</value1> + * <value2>XYZ</value2> + * </prefs> + * </query> + * </iq></pre> + * + * <p>Note: this method is generally only called by the internal Smack classes. + * + * @param elementName the XML element name. + * @param namespace the XML namespace. + * @return the PrivateData provider. + */ + public static PrivateDataProvider getPrivateDataProvider(String elementName, String namespace) { + String key = getProviderKey(elementName, namespace); + return (PrivateDataProvider)privateDataProviders.get(key); + } + + /** + * Adds a private data provider with the specified element name and name space. The provider + * will override any providers loaded through the classpath. + * + * @param elementName the XML element name. + * @param namespace the XML namespace. + * @param provider the private data provider. + */ + public static void addPrivateDataProvider(String elementName, String namespace, + PrivateDataProvider provider) + { + String key = getProviderKey(elementName, namespace); + privateDataProviders.put(key, provider); + } + + + private XMPPConnection connection; + + /** + * The user to get and set private data for. In most cases, this value should + * be <tt>null</tt>, as the typical use of private data is to get and set + * your own private data and not others. + */ + private String user; + + /** + * Creates a new private data manager. The connection must have + * undergone a successful login before being used to construct an instance of + * this class. + * + * @param connection an XMPP connection which must have already undergone a + * successful login. + */ + public PrivateDataManager(XMPPConnection connection) { + if (!connection.isAuthenticated()) { + throw new IllegalStateException("Must be logged in to XMPP server."); + } + this.connection = connection; + } + + /** + * Creates a new private data manager for a specific user (special case). Most + * servers only support getting and setting private data for the user that + * authenticated via the connection. However, some servers support the ability + * to get and set private data for other users (for example, if you are the + * administrator). The connection must have undergone a successful login before + * being used to construct an instance of this class. + * + * @param connection an XMPP connection which must have already undergone a + * successful login. + * @param user the XMPP address of the user to get and set private data for. + */ + public PrivateDataManager(XMPPConnection connection, String user) { + if (!connection.isAuthenticated()) { + throw new IllegalStateException("Must be logged in to XMPP server."); + } + this.connection = connection; + this.user = user; + } + + /** + * Returns the private data specified by the given element name and namespace. Each chunk + * of private data is uniquely identified by an element name and namespace pair.<p> + * + * If a PrivateDataProvider is registered for the specified element name/namespace pair then + * that provider will determine the specific object type that is returned. If no provider + * is registered, a {@link DefaultPrivateData} instance will be returned. + * + * @param elementName the element name. + * @param namespace the namespace. + * @return the private data. + * @throws XMPPException if an error occurs getting the private data. + */ + public PrivateData getPrivateData(final String elementName, final String namespace) + throws XMPPException + { + // Create an IQ packet to get the private data. + IQ privateDataGet = new IQ() { + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"jabber:iq:private\">"); + buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\"/>"); + buf.append("</query>"); + return buf.toString(); + } + }; + privateDataGet.setType(IQ.Type.GET); + // Address the packet to the other account if user has been set. + if (user != null) { + privateDataGet.setTo(user); + } + + // Setup a listener for the reply to the set operation. + String packetID = privateDataGet.getPacketID(); + PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID)); + + // Send the private data. + connection.sendPacket(privateDataGet); + + // Wait up to five seconds for a response from the server. + IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from the server."); + } + // If the server replied with an error, throw an exception. + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + return ((PrivateDataResult)response).getPrivateData(); + } + + /** + * Sets a private data value. Each chunk of private data is uniquely identified by an + * element name and namespace pair. If private data has already been set with the + * element name and namespace, then the new private data will overwrite the old value. + * + * @param privateData the private data. + * @throws XMPPException if setting the private data fails. + */ + public void setPrivateData(final PrivateData privateData) throws XMPPException { + // Create an IQ packet to set the private data. + IQ privateDataSet = new IQ() { + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"jabber:iq:private\">"); + buf.append(privateData.toXML()); + buf.append("</query>"); + return buf.toString(); + } + }; + privateDataSet.setType(IQ.Type.SET); + // Address the packet to the other account if user has been set. + if (user != null) { + privateDataSet.setTo(user); + } + + // Setup a listener for the reply to the set operation. + String packetID = privateDataSet.getPacketID(); + PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID)); + + // Send the private data. + connection.sendPacket(privateDataSet); + + // Wait up to five seconds for a response from the server. + IQ response = (IQ)collector.nextResult(5000); + // Stop queuing results + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from the server."); + } + // If the server replied with an error, throw an exception. + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + } + + /** + * Returns a String key for a given element name and namespace. + * + * @param elementName the element name. + * @param namespace the namespace. + * @return a unique key for the element name and namespace pair. + */ + private static String getProviderKey(String elementName, String namespace) { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(elementName).append("/><").append(namespace).append("/>"); + return buf.toString(); + } + + /** + * An IQ provider to parse IQ results containing private data. + */ + public static class PrivateDataIQProvider implements IQProvider { + public IQ parseIQ(XmlPullParser parser) throws Exception { + PrivateData privateData = null; + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + // See if any objects are registered to handle this private data type. + PrivateDataProvider provider = getPrivateDataProvider(elementName, namespace); + // If there is a registered provider, use it. + if (provider != null) { + privateData = provider.parsePrivateData(parser); + } + // Otherwise, use a DefaultPrivateData instance to store the private data. + else { + DefaultPrivateData data = new DefaultPrivateData(elementName, namespace); + boolean finished = false; + while (!finished) { + int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + String name = parser.getName(); + // If an empty element, set the value with the empty string. + if (parser.isEmptyElementTag()) { + data.setValue(name,""); + } + // Otherwise, get the the element text. + else { + event = parser.next(); + if (event == XmlPullParser.TEXT) { + String value = parser.getText(); + data.setValue(name, value); + } + } + } + else if (event == XmlPullParser.END_TAG) { + if (parser.getName().equals(elementName)) { + finished = true; + } + } + } + privateData = data; + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + IQ result = new PrivateDataResult(privateData); + return result; + } + } + + /** + * An IQ packet to hold PrivateData GET results. + */ + private static class PrivateDataResult extends IQ { + + private PrivateData privateData; + + PrivateDataResult(PrivateData privateData) { + this.privateData = privateData; + } + + public PrivateData getPrivateData() { + return privateData; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"jabber:iq:private\">"); + if (privateData != null) { + privateData.toXML(); + } + buf.append("</query>"); + return buf.toString(); + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RemoteRosterEntry.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RemoteRosterEntry.java new file mode 100644 index 000000000..e8c62de31 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RemoteRosterEntry.java @@ -0,0 +1,118 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import java.util.*; + +/** + * Represents a roster item, which consists of a JID and , their name and + * the groups the roster item belongs to. This roster item does not belong + * to the local roster. Therefore, it does not persist in the server.<p> + * + * The idea of a RemoteRosterEntry is to be used as part of a roster exchange. + * + * @author Gaston Dombiak + */ +public class RemoteRosterEntry { + + private String user; + private String name; + private List groupNames = new ArrayList(); + + /** + * Creates a new remote roster entry. + * + * @param user the user. + * @param name the user's name. + * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the + * the roster entry won't belong to a group. + */ + public RemoteRosterEntry(String user, String name, String [] groups) { + this.user = user; + this.name = name; + if (groups != null) { + groupNames = new ArrayList(Arrays.asList(groups)); + } + } + + /** + * Returns the user. + * + * @return the user. + */ + public String getUser() { + return user; + } + + /** + * Returns the user's name. + * + * @return the user's name. + */ + public String getName() { + return name; + } + + /** + * Returns an Iterator for the group names (as Strings) that the roster entry + * belongs to. + * + * @return an Iterator for the group names. + */ + public Iterator getGroupNames() { + synchronized (groupNames) { + return Collections.unmodifiableList(groupNames).iterator(); + } + } + + /** + * Returns a String array for the group names that the roster entry + * belongs to. + * + * @return a String[] for the group names. + */ + public String[] getGroupArrayNames() { + synchronized (groupNames) { + return (String[]) + (Collections + .unmodifiableList(groupNames) + .toArray(new String[groupNames.size()])); + } + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<item jid=\"").append(user).append("\""); + if (name != null) { + buf.append(" name=\"").append(name).append("\""); + } + buf.append(">"); + synchronized (groupNames) { + for (int i = 0; i < groupNames.size(); i++) { + String groupName = (String) groupNames.get(i); + buf.append("<group>").append(groupName).append("</group>"); + } + } + buf.append("</item>"); + return buf.toString(); + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ReportedData.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ReportedData.java new file mode 100644 index 000000000..93e5fd140 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ReportedData.java @@ -0,0 +1,277 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import java.util.*; + +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smackx.packet.DataForm; + +/** + * Represents a set of data results returned as part of a search. The report is structured + * in columns and rows. + * + * @author Gaston Dombiak + */ +public class ReportedData { + + private List columns = new ArrayList(); + private List rows = new ArrayList(); + private String title = ""; + + /** + * Returns a new ReportedData if the packet is used for reporting data and includes an + * extension that matches the elementName and namespace "x","jabber:x:data". + * + * @param packet the packet used for reporting data. + */ + public static ReportedData getReportedDataFrom(Packet packet) { + // Check if the packet includes the DataForm extension + PacketExtension packetExtension = packet.getExtension("x","jabber:x:data"); + if (packetExtension != null) { + // Check if the existing DataForm is a result of a search + DataForm dataForm = (DataForm) packetExtension; + if (dataForm.getReportedData() != null) + return new ReportedData(dataForm); + } + // Otherwise return null + return null; + } + + + /** + * Creates a new ReportedData based on the returned dataForm from a search + *(namespace "jabber:iq:search"). + * + * @param dataForm the dataForm returned from a search (namespace "jabber:iq:search"). + */ + private ReportedData(DataForm dataForm) { + // Add the columns to the report based on the reported data fields + for (Iterator fields = dataForm.getReportedData().getFields(); fields.hasNext();) { + FormField field = (FormField)fields.next(); + columns.add(new Column(field.getLabel(), field.getVariable(), field.getType())); + } + + // Add the rows to the report based on the form's items + for (Iterator items = dataForm.getItems(); items.hasNext();) { + DataForm.Item item = (DataForm.Item)items.next(); + List fieldList = new ArrayList(columns.size()); + FormField field; + for (Iterator fields = item.getFields(); fields.hasNext();) { + field = (FormField) fields.next(); + // The field is created with all the values of the data form's field + List values = new ArrayList(); + for (Iterator it=field.getValues(); it.hasNext();) { + values.add(it.next()); + } + fieldList.add(new Field(field.getVariable(), values)); + } + rows.add(new Row(fieldList)); + } + + // Set the report's title + this.title = dataForm.getTitle(); + } + + + public ReportedData(){ + // Allow for model creation of ReportedData. + } + + /** + * Adds a new <code>Row</code>. + * @param row the new row to add. + */ + public void addRow(Row row){ + rows.add(row); + } + + /** + * Adds a new <code>Column</code> + * @param column the column to add. + */ + public void addColumn(Column column){ + columns.add(column); + } + + + /** + * Returns an Iterator for the rows returned from a search. + * + * @return an Iterator for the rows returned from a search. + */ + public Iterator getRows() { + return Collections.unmodifiableList(new ArrayList(rows)).iterator(); + } + + /** + * Returns an Iterator for the columns returned from a search. + * + * @return an Iterator for the columns returned from a search. + */ + public Iterator getColumns() { + return Collections.unmodifiableList(new ArrayList(columns)).iterator(); + } + + + /** + * Returns the report's title. It is similar to the title on a web page or an X + * window. + * + * @return title of the report. + */ + public String getTitle() { + return title; + } + + /** + * + * Represents the columns definition of the reported data. + * + * @author Gaston Dombiak + */ + public static class Column { + private String label; + private String variable; + private String type; + + /** + * Creates a new column with the specified definition. + * + * @param label the columns's label. + * @param variable the variable name of the column. + * @param type the format for the returned data. + */ + public Column(String label, String variable, String type) { + this.label = label; + this.variable = variable; + this.type = type; + } + + /** + * Returns the column's label. + * + * @return label of the column. + */ + public String getLabel() { + return label; + } + + + /** + * Returns the column's data format. Valid formats are: + * + * <ul> + * <li>text-single -> single line or word of text + * <li>text-private -> instead of showing the user what they typed, you show ***** to + * protect it + * <li>text-multi -> multiple lines of text entry + * <li>list-single -> given a list of choices, pick one + * <li>list-multi -> given a list of choices, pick one or more + * <li>boolean -> 0 or 1, true or false, yes or no. Default value is 0 + * <li>fixed -> fixed for putting in text to show sections, or just advertise your web + * site in the middle of the form + * <li>hidden -> is not given to the user at all, but returned with the questionnaire + * <li>jid-single -> Jabber ID - choosing a JID from your roster, and entering one based + * on the rules for a JID. + * <li>jid-multi -> multiple entries for JIDs + * </ul> + * + * @return format for the returned data. + */ + public String getType() { + return type; + } + + + /** + * Returns the variable name that the column is showing. + * + * @return the variable name of the column. + */ + public String getVariable() { + return variable; + } + + + } + + public static class Row { + private List fields = new ArrayList(); + + public Row(List fields) { + this.fields = fields; + } + + /** + * Returns the values of the field whose variable matches the requested variable. + * + * @param variable the variable to match. + * @return the values of the field whose variable matches the requested variable. + */ + public Iterator getValues(String variable) { + for(Iterator it=getFields();it.hasNext();) { + Field field = (Field) it.next(); + if (variable.equalsIgnoreCase(field.getVariable())) { + return field.getValues(); + } + } + return null; + } + + /** + * Returns the fields that define the data that goes with the item. + * + * @return the fields that define the data that goes with the item. + */ + private Iterator getFields() { + return Collections.unmodifiableList(new ArrayList(fields)).iterator(); + } + } + + public static class Field { + private String variable; + private List values; + + public Field(String variable, List values) { + this.variable = variable; + this.values = values; + } + + /** + * Returns the variable name that the field represents. + * + * @return the variable name of the field. + */ + public String getVariable() { + return variable; + } + + /** + * Returns an iterator on the values reported as part of the search. + * + * @return the returned values of the search. + */ + public Iterator getValues() { + return Collections.unmodifiableList(values).iterator(); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeListener.java new file mode 100644 index 000000000..2c7460b62 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeListener.java @@ -0,0 +1,42 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import java.util.Iterator; + +/** + * + * A listener that is fired anytime a roster exchange is received. + * + * @author Gaston Dombiak + */ +public interface RosterExchangeListener { + + /** + * Called when roster entries are received as part of a roster exchange. + * + * @param from the user that sent the entries. + * @param remoteRosterEntries the entries sent by the user. The entries are instances of + * RemoteRosterEntry. + */ + public void entriesReceived(String from, Iterator remoteRosterEntries); + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeManager.java new file mode 100644 index 000000000..66c4f477f --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/RosterExchangeManager.java @@ -0,0 +1,177 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import java.util.*; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.filter.*; +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smackx.packet.RosterExchange; + +/** + * + * Manages Roster exchanges. A RosterExchangeManager provides a high level access to send + * rosters, roster groups and roster entries to XMPP clients. It also provides an easy way + * to hook up custom logic when entries are received from another XMPP client through + * RosterExchangeListeners. + * + * @author Gaston Dombiak + */ +public class RosterExchangeManager { + + private List rosterExchangeListeners = new ArrayList(); + + private XMPPConnection con; + + private PacketFilter packetFilter = new PacketExtensionFilter("x", "jabber:x:roster"); + private PacketListener packetListener; + + /** + * Creates a new roster exchange manager. + * + * @param con an XMPPConnection. + */ + public RosterExchangeManager(XMPPConnection con) { + this.con = con; + init(); + } + + /** + * Adds a listener to roster exchanges. The listener will be fired anytime roster entries + * are received from remote XMPP clients. + * + * @param rosterExchangeListener a roster exchange listener. + */ + public void addRosterListener(RosterExchangeListener rosterExchangeListener) { + synchronized (rosterExchangeListeners) { + if (!rosterExchangeListeners.contains(rosterExchangeListener)) { + rosterExchangeListeners.add(rosterExchangeListener); + } + } + } + + /** + * Removes a listener from roster exchanges. The listener will be fired anytime roster + * entries are received from remote XMPP clients. + * + * @param rosterExchangeListener a roster exchange listener.. + */ + public void removeRosterListener(RosterExchangeListener rosterExchangeListener) { + synchronized (rosterExchangeListeners) { + rosterExchangeListeners.remove(rosterExchangeListener); + } + } + + /** + * Sends a roster to userID. All the entries of the roster will be sent to the + * target user. + * + * @param roster the roster to send + * @param targetUserID the user that will receive the roster entries + */ + public void send(Roster roster, String targetUserID) { + // Create a new message to send the roster + Message msg = new Message(targetUserID); + // Create a RosterExchange Package and add it to the message + RosterExchange rosterExchange = new RosterExchange(roster); + msg.addExtension(rosterExchange); + + // Send the message that contains the roster + con.sendPacket(msg); + } + + /** + * Sends a roster entry to userID. + * + * @param rosterEntry the roster entry to send + * @param targetUserID the user that will receive the roster entries + */ + public void send(RosterEntry rosterEntry, String targetUserID) { + // Create a new message to send the roster + Message msg = new Message(targetUserID); + // Create a RosterExchange Package and add it to the message + RosterExchange rosterExchange = new RosterExchange(); + rosterExchange.addRosterEntry(rosterEntry); + msg.addExtension(rosterExchange); + + // Send the message that contains the roster + con.sendPacket(msg); + } + + /** + * Sends a roster group to userID. All the entries of the group will be sent to the + * target user. + * + * @param rosterGroup the roster group to send + * @param targetUserID the user that will receive the roster entries + */ + public void send(RosterGroup rosterGroup, String targetUserID) { + // Create a new message to send the roster + Message msg = new Message(targetUserID); + // Create a RosterExchange Package and add it to the message + RosterExchange rosterExchange = new RosterExchange(); + for (Iterator it = rosterGroup.getEntries(); it.hasNext();) + rosterExchange.addRosterEntry((RosterEntry) it.next()); + msg.addExtension(rosterExchange); + + // Send the message that contains the roster + con.sendPacket(msg); + } + + /** + * Fires roster exchange listeners. + */ + private void fireRosterExchangeListeners(String from, Iterator remoteRosterEntries) { + RosterExchangeListener[] listeners = null; + synchronized (rosterExchangeListeners) { + listeners = new RosterExchangeListener[rosterExchangeListeners.size()]; + rosterExchangeListeners.toArray(listeners); + } + for (int i = 0; i < listeners.length; i++) { + listeners[i].entriesReceived(from, remoteRosterEntries); + } + } + + private void init() { + // Listens for all roster exchange packets and fire the roster exchange listeners. + packetListener = new PacketListener() { + public void processPacket(Packet packet) { + Message message = (Message) packet; + RosterExchange rosterExchange = + (RosterExchange) message.getExtension("x", "jabber:x:roster"); + // Fire event for roster exchange listeners + fireRosterExchangeListeners(message.getFrom(), rosterExchange.getRosterEntries()); + }; + + }; + con.addPacketListener(packetListener, packetFilter); + } + + public void destroy() { + if (con != null) + con.removePacketListener(packetListener); + + } + public void finalize() { + destroy(); + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java new file mode 100644 index 000000000..94b2ea4cf --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java @@ -0,0 +1,483 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import java.util.*; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.filter.*; +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smackx.packet.*; + +/** + * Manages discovery of services in XMPP entities. This class provides: + * <ol> + * <li>A registry of supported features in this XMPP entity. + * <li>Automatic response when this XMPP entity is queried for information. + * <li>Ability to discover items and information of remote XMPP entities. + * <li>Ability to publish publicly available items. + * </ol> + * + * @author Gaston Dombiak + */ +public class ServiceDiscoveryManager { + + private static String identityName = "Smack"; + private static String identityType = "pc"; + + private static Map instances = new Hashtable(); + + private XMPPConnection connection; + private List features = new ArrayList(); + private Map nodeInformationProviders = new Hashtable(); + + // Create a new ServiceDiscoveryManager on every established connection + static { + XMPPConnection.addConnectionListener(new ConnectionEstablishedListener() { + public void connectionEstablished(XMPPConnection connection) { + new ServiceDiscoveryManager(connection); + } + }); + } + + /** + * Creates a new ServiceDiscoveryManager for a given XMPPConnection. This means that the + * service manager will respond to any service discovery request that the connection may + * receive. + * + * @param connection the connection to which a ServiceDiscoveryManager is going to be created. + */ + public ServiceDiscoveryManager(XMPPConnection connection) { + this.connection = connection; + init(); + } + + /** + * Returns the ServiceDiscoveryManager instance associated with a given XMPPConnection. + * + * @param connection the connection used to look for the proper ServiceDiscoveryManager. + * @return the ServiceDiscoveryManager associated with a given XMPPConnection. + */ + public static ServiceDiscoveryManager getInstanceFor(XMPPConnection connection) { + return (ServiceDiscoveryManager) instances.get(connection); + } + + /** + * Returns the name of the client that will be returned when asked for the client identity + * in a disco request. The name could be any value you need to identity this client. + * + * @return the name of the client that will be returned when asked for the client identity + * in a disco request. + */ + public static String getIdentityName() { + return identityName; + } + + /** + * Sets the name of the client that will be returned when asked for the client identity + * in a disco request. The name could be any value you need to identity this client. + * + * @param name the name of the client that will be returned when asked for the client identity + * in a disco request. + */ + public static void setIdentityName(String name) { + identityName = name; + } + + /** + * Returns the type of client that will be returned when asked for the client identity in a + * disco request. The valid types are defined by the category client. Follow this link to learn + * the possible types: <a href="http://www.jabber.org/registrar/disco-categories.html#client">Jabber::Registrar</a>. + * + * @return the type of client that will be returned when asked for the client identity in a + * disco request. + */ + public static String getIdentityType() { + return identityType; + } + + /** + * Sets the type of client that will be returned when asked for the client identity in a + * disco request. The valid types are defined by the category client. Follow this link to learn + * the possible types: <a href="http://www.jabber.org/registrar/disco-categories.html#client">Jabber::Registrar</a>. + * + * @param type the type of client that will be returned when asked for the client identity in a + * disco request. + */ + public static void setIdentityType(String type) { + identityType = type; + } + + /** + * Initializes the packet listeners of the connection that will answer to any + * service discovery request. + */ + private void init() { + // Register the new instance and associate it with the connection + instances.put(connection, this); + // Add a listener to the connection that removes the registered instance when + // the connection is closed + connection.addConnectionListener(new ConnectionListener() { + public void connectionClosed() { + // Unregister this instance since the connection has been closed + instances.remove(connection); + } + + public void connectionClosedOnError(Exception e) { + // Unregister this instance since the connection has been closed + instances.remove(connection); + } + }); + + // Listen for disco#items requests and answer with an empty result + PacketFilter packetFilter = new PacketTypeFilter(DiscoverItems.class); + PacketListener packetListener = new PacketListener() { + public void processPacket(Packet packet) { + DiscoverItems discoverItems = (DiscoverItems) packet; + // Send back the items defined in the client if the request is of type GET + if (discoverItems != null && discoverItems.getType() == IQ.Type.GET) { + DiscoverItems response = new DiscoverItems(); + response.setType(IQ.Type.RESULT); + response.setTo(discoverItems.getFrom()); + response.setPacketID(discoverItems.getPacketID()); + + // Add the defined items related to the requested node. Look for + // the NodeInformationProvider associated with the requested node. + if (getNodeInformationProvider(discoverItems.getNode()) != null) { + Iterator items = + getNodeInformationProvider(discoverItems.getNode()).getNodeItems(); + while (items.hasNext()) { + response.addItem((DiscoverItems.Item) items.next()); + } + } else if(discoverItems.getNode() != null) { + // Return an <item-not-found/> error since the client + // doesn't contain the specified node + response.setNode(discoverItems.getNode()); + response.setType(IQ.Type.ERROR); + response.setError(new XMPPError(404, "item-not-found")); + } + connection.sendPacket(response); + } + } + }; + connection.addPacketListener(packetListener, packetFilter); + + // Listen for disco#info requests and answer the client's supported features + // To add a new feature as supported use the #addFeature message + packetFilter = new PacketTypeFilter(DiscoverInfo.class); + packetListener = new PacketListener() { + public void processPacket(Packet packet) { + DiscoverInfo discoverInfo = (DiscoverInfo) packet; + // Answer the client's supported features if the request is of the GET type + if (discoverInfo != null && discoverInfo.getType() == IQ.Type.GET) { + DiscoverInfo response = new DiscoverInfo(); + response.setType(IQ.Type.RESULT); + response.setTo(discoverInfo.getFrom()); + response.setPacketID(discoverInfo.getPacketID()); + // Add the client's identity and features only if "node" is null + if (discoverInfo.getNode() == null) { + // Set this client identity + DiscoverInfo.Identity identity = new DiscoverInfo.Identity("client", + getIdentityName()); + identity.setType(getIdentityType()); + response.addIdentity(identity); + // Add the registered features to the response + synchronized (features) { + for (Iterator it = getFeatures(); it.hasNext();) { + response.addFeature((String) it.next()); + } + } + } + else { + // Return an <item-not-found/> error since a client doesn't have nodes + response.setNode(discoverInfo.getNode()); + response.setType(IQ.Type.ERROR); + response.setError(new XMPPError(404, "item-not-found")); + } + connection.sendPacket(response); + } + } + }; + connection.addPacketListener(packetListener, packetFilter); + } + + /** + * Returns the NodeInformationProvider responsible for providing information + * (ie items) related to a given node or <tt>null</null> if none.<p> + * + * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the + * NodeInformationProvider will provide information about the rooms where the user has joined. + * + * @param node the node that contains items associated with an entity not addressable as a JID. + * @return the NodeInformationProvider responsible for providing information related + * to a given node. + */ + private NodeInformationProvider getNodeInformationProvider(String node) { + if (node == null) { + return null; + } + return (NodeInformationProvider) nodeInformationProviders.get(node); + } + + /** + * Sets the NodeInformationProvider responsible for providing information + * (ie items) related to a given node. Every time this client receives a disco request + * regarding the items of a given node, the provider associated to that node will be the + * responsible for providing the requested information.<p> + * + * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the + * NodeInformationProvider will provide information about the rooms where the user has joined. + * + * @param node the node whose items will be provided by the NodeInformationProvider. + * @param listener the NodeInformationProvider responsible for providing items related + * to the node. + */ + public void setNodeInformationProvider(String node, NodeInformationProvider listener) { + nodeInformationProviders.put(node, listener); + } + + /** + * Removes the NodeInformationProvider responsible for providing information + * (ie items) related to a given node. This means that no more information will be + * available for the specified node. + * + * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the + * NodeInformationProvider will provide information about the rooms where the user has joined. + * + * @param node the node to remove the associated NodeInformationProvider. + */ + public void removeNodeInformationProvider(String node) { + nodeInformationProviders.remove(node); + } + + /** + * Returns the supported features by this XMPP entity. + * + * @return an Iterator on the supported features by this XMPP entity. + */ + public Iterator getFeatures() { + synchronized (features) { + return Collections.unmodifiableList(new ArrayList(features)).iterator(); + } + } + + /** + * Registers that a new feature is supported by this XMPP entity. When this client is + * queried for its information the registered features will be answered.<p> + * + * Since no packet is actually sent to the server it is safe to perform this operation + * before logging to the server. In fact, you may want to configure the supported features + * before logging to the server so that the information is already available if it is required + * upon login. + * + * @param feature the feature to register as supported. + */ + public void addFeature(String feature) { + synchronized (features) { + features.add(feature); + } + } + + /** + * Removes the specified feature from the supported features by this XMPP entity.<p> + * + * Since no packet is actually sent to the server it is safe to perform this operation + * before logging to the server. + * + * @param feature the feature to remove from the supported features. + */ + public void removeFeature(String feature) { + synchronized (features) { + features.remove(feature); + } + } + + /** + * Returns true if the specified feature is registered in the ServiceDiscoveryManager. + * + * @param feature the feature to look for. + * @return a boolean indicating if the specified featured is registered or not. + */ + public boolean includesFeature(String feature) { + synchronized (features) { + return features.contains(feature); + } + } + + /** + * Returns the discovered information of a given XMPP entity addressed by its JID. + * + * @param entityID the address of the XMPP entity. + * @return the discovered information. + * @throws XMPPException if the operation failed for some reason. + */ + public DiscoverInfo discoverInfo(String entityID) throws XMPPException { + return discoverInfo(entityID, null); + } + + /** + * Returns the discovered information of a given XMPP entity addressed by its JID and + * note attribute. Use this message only when trying to query information which is not + * directly addressable. + * + * @param entityID the address of the XMPP entity. + * @param node the attribute that supplements the 'jid' attribute. + * @return the discovered information. + * @throws XMPPException if the operation failed for some reason. + */ + public DiscoverInfo discoverInfo(String entityID, String node) throws XMPPException { + // Discover the entity's info + DiscoverInfo disco = new DiscoverInfo(); + disco.setType(IQ.Type.GET); + disco.setTo(entityID); + disco.setNode(node); + + // Create a packet collector to listen for a response. + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(disco.getPacketID())); + + connection.sendPacket(disco); + + // Wait up to 5 seconds for a result. + IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from the server."); + } + if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + return (DiscoverInfo) result; + } + + /** + * Returns the discovered items of a given XMPP entity addressed by its JID. + * + * @param entityID the address of the XMPP entity. + * @return the discovered information. + * @throws XMPPException if the operation failed for some reason. + */ + public DiscoverItems discoverItems(String entityID) throws XMPPException { + return discoverItems(entityID, null); + } + + /** + * Returns the discovered items of a given XMPP entity addressed by its JID and + * note attribute. Use this message only when trying to query information which is not + * directly addressable. + * + * @param entityID the address of the XMPP entity. + * @param node the attribute that supplements the 'jid' attribute. + * @return the discovered items. + * @throws XMPPException if the operation failed for some reason. + */ + public DiscoverItems discoverItems(String entityID, String node) throws XMPPException { + // Discover the entity's items + DiscoverItems disco = new DiscoverItems(); + disco.setType(IQ.Type.GET); + disco.setTo(entityID); + disco.setNode(node); + + // Create a packet collector to listen for a response. + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(disco.getPacketID())); + + connection.sendPacket(disco); + + // Wait up to 5 seconds for a result. + IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from the server."); + } + if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + return (DiscoverItems) result; + } + + /** + * Returns true if the server supports publishing of items. A client may wish to publish items + * to the server so that the server can provide items associated to the client. These items will + * be returned by the server whenever the server receives a disco request targeted to the bare + * address of the client (i.e. user@host.com). + * + * @param entityID the address of the XMPP entity. + * @return true if the server supports publishing of items. + * @throws XMPPException if the operation failed for some reason. + */ + public boolean canPublishItems(String entityID) throws XMPPException { + DiscoverInfo info = discoverInfo(entityID); + return info.containsFeature("http://jabber.org/protocol/disco#publish"); + } + + /** + * Publishes new items to a parent entity. The item elements to publish MUST have at least + * a 'jid' attribute specifying the Entity ID of the item, and an action attribute which + * specifies the action being taken for that item. Possible action values are: "update" and + * "remove". + * + * @param entityID the address of the XMPP entity. + * @param discoverItems the DiscoveryItems to publish. + * @throws XMPPException if the operation failed for some reason. + */ + public void publishItems(String entityID, DiscoverItems discoverItems) + throws XMPPException { + publishItems(entityID, null, discoverItems); + } + + /** + * Publishes new items to a parent entity and node. The item elements to publish MUST have at + * least a 'jid' attribute specifying the Entity ID of the item, and an action attribute which + * specifies the action being taken for that item. Possible action values are: "update" and + * "remove". + * + * @param entityID the address of the XMPP entity. + * @param node the attribute that supplements the 'jid' attribute. + * @param discoverItems the DiscoveryItems to publish. + * @throws XMPPException if the operation failed for some reason. + */ + public void publishItems(String entityID, String node, DiscoverItems discoverItems) + throws XMPPException { + discoverItems.setType(IQ.Type.SET); + discoverItems.setTo(entityID); + discoverItems.setNode(node); + + // Create a packet collector to listen for a response. + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(discoverItems.getPacketID())); + + connection.sendPacket(discoverItems); + + // Wait up to 5 seconds for a result. + IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from the server."); + } + if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/SharedGroupManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/SharedGroupManager.java new file mode 100644 index 000000000..f69f30708 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/SharedGroupManager.java @@ -0,0 +1,53 @@ +package org.jivesoftware.smackx; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.packet.SharedGroupsInfo; + +import java.util.List; + +/** + * A SharedGroupManager provides services for discovering the shared groups where a user belongs.<p> + * + * Important note: This functionality is not part of the XMPP spec and it will only work + * with Wildfire. + * + * @author Gaston Dombiak + */ +public class SharedGroupManager { + + /** + * Returns the collection that will contain the name of the shared groups where the user + * logged in with the specified session belongs. + * + * @param connection connection to use to get the user's shared groups. + * @return collection with the shared groups' name of the logged user. + */ + public static List getSharedGroups(XMPPConnection connection) throws XMPPException { + // Discover the shared groups of the logged user + SharedGroupsInfo info = new SharedGroupsInfo(); + info.setType(IQ.Type.GET); + + // Create a packet collector to listen for a response. + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(info.getPacketID())); + + connection.sendPacket(info); + + // Wait up to 5 seconds for a result. + IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from the server."); + } + if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + return ((SharedGroupsInfo) result).getGroups(); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLManager.java new file mode 100644 index 000000000..064c03b43 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLManager.java @@ -0,0 +1,141 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import java.util.Iterator; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.packet.*; + +/** + * Manages XHTML formatted texts within messages. A XHTMLManager provides a high level access to + * get and set XHTML bodies to messages, enable and disable XHTML support and check if remote XMPP + * clients support XHTML. + * + * @author Gaston Dombiak + */ +public class XHTMLManager { + + private final static String namespace = "http://jabber.org/protocol/xhtml-im"; + + // Enable the XHTML support on every established connection + // The ServiceDiscoveryManager class should have been already initialized + static { + XMPPConnection.addConnectionListener(new ConnectionEstablishedListener() { + public void connectionEstablished(XMPPConnection connection) { + XHTMLManager.setServiceEnabled(connection, true); + } + }); + } + + /** + * Returns an Iterator for the XHTML bodies in the message. Returns null if + * the message does not contain an XHTML extension. + * + * @param message an XHTML message + * @return an Iterator for the bodies in the message or null if none. + */ + public static Iterator getBodies(Message message) { + XHTMLExtension xhtmlExtension = (XHTMLExtension) message.getExtension("html", namespace); + if (xhtmlExtension != null) + return xhtmlExtension.getBodies(); + else + return null; + } + + /** + * Adds an XHTML body to the message. + * + * @param message the message that will receive the XHTML body + * @param body the string to add as an XHTML body to the message + */ + public static void addBody(Message message, String body) { + XHTMLExtension xhtmlExtension = (XHTMLExtension) message.getExtension("html", namespace); + if (xhtmlExtension == null) { + // Create an XHTMLExtension and add it to the message + xhtmlExtension = new XHTMLExtension(); + message.addExtension(xhtmlExtension); + } + // Add the required bodies to the message + xhtmlExtension.addBody(body); + } + + /** + * Returns true if the message contains an XHTML extension. + * + * @param message the message to check if contains an XHTML extentsion or not + * @return a boolean indicating whether the message is an XHTML message + */ + public static boolean isXHTMLMessage(Message message) { + return message.getExtension("html", namespace) != null; + } + + /** + * Enables or disables the XHTML support on a given connection.<p> + * + * Before starting to send XHTML messages to a user, check that the user can handle XHTML + * messages. Enable the XHTML support to indicate that this client handles XHTML messages. + * + * @param connection the connection where the service will be enabled or disabled + * @param enabled indicates if the service will be enabled or disabled + */ + public synchronized static void setServiceEnabled(XMPPConnection connection, boolean enabled) { + if (isServiceEnabled(connection) == enabled) + return; + + if (enabled) { + ServiceDiscoveryManager.getInstanceFor(connection).addFeature(namespace); + } + else { + ServiceDiscoveryManager.getInstanceFor(connection).removeFeature(namespace); + } + } + + /** + * Returns true if the XHTML support is enabled for the given connection. + * + * @param connection the connection to look for XHTML support + * @return a boolean indicating if the XHTML support is enabled for the given connection + */ + public static boolean isServiceEnabled(XMPPConnection connection) { + return ServiceDiscoveryManager.getInstanceFor(connection).includesFeature(namespace); + } + + /** + * Returns true if the specified user handles XHTML messages. + * + * @param connection the connection to use to perform the service discovery + * @param userID the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com + * @return a boolean indicating whether the specified user handles XHTML messages + */ + public static boolean isServiceEnabled(XMPPConnection connection, String userID) { + try { + DiscoverInfo result = + ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(userID); + return result.containsFeature(namespace); + } + catch (XMPPException e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLText.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLText.java new file mode 100644 index 000000000..ee67e2880 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/XHTMLText.java @@ -0,0 +1,429 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx; + +import org.jivesoftware.smack.util.StringUtils; + +/** + * An XHTMLText represents formatted text. This class also helps to build valid + * XHTML tags. + * + * @author Gaston Dombiak + */ +public class XHTMLText { + + private StringBuffer text = new StringBuffer(30); + + /** + * Creates a new XHTMLText with body tag params. + * + * @param style the XHTML style of the body + * @param lang the language of the body + */ + public XHTMLText(String style, String lang) { + appendOpenBodyTag(style, lang); + } + + /** + * Appends a tag that indicates that an anchor section begins. + * + * @param href indicates the URL being linked to + * @param style the XHTML style of the anchor + */ + public void appendOpenAnchorTag(String href, String style) { + StringBuffer sb = new StringBuffer("<a"); + if (href != null) { + sb.append(" href=\""); + sb.append(href); + sb.append("\""); + } + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that an anchor section ends. + * + */ + public void appendCloseAnchorTag() { + text.append("</a>"); + } + + /** + * Appends a tag that indicates that a blockquote section begins. + * + * @param style the XHTML style of the blockquote + */ + public void appendOpenBlockQuoteTag(String style) { + StringBuffer sb = new StringBuffer("<blockquote"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that a blockquote section ends. + * + */ + public void appendCloseBlockQuoteTag() { + text.append("</blockquote>"); + } + + /** + * Appends a tag that indicates that a body section begins. + * + * @param style the XHTML style of the body + * @param lang the language of the body + */ + private void appendOpenBodyTag(String style, String lang) { + StringBuffer sb = new StringBuffer("<body"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + if (lang != null) { + sb.append(" xml:lang=\""); + sb.append(lang); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that a body section ends. + * + */ + private String closeBodyTag() { + return "</body>"; + } + + /** + * Appends a tag that inserts a single carriage return. + * + */ + public void appendBrTag() { + text.append("<br>"); + } + + /** + * Appends a tag that indicates a reference to work, such as a book, report or web site. + * + */ + public void appendOpenCiteTag() { + text.append("<cite>"); + } + + /** + * Appends a tag that indicates text that is the code for a program. + * + */ + public void appendOpenCodeTag() { + text.append("<code>"); + } + + /** + * Appends a tag that indicates end of text that is the code for a program. + * + */ + public void appendCloseCodeTag() { + text.append("</code>"); + } + + /** + * Appends a tag that indicates emphasis. + * + */ + public void appendOpenEmTag() { + text.append("<em>"); + } + + /** + * Appends a tag that indicates end of emphasis. + * + */ + public void appendCloseEmTag() { + text.append("</em>"); + } + + /** + * Appends a tag that indicates a header, a title of a section of the message. + * + * @param level the level of the Header. It should be a value between 1 and 3 + * @param style the XHTML style of the blockquote + */ + public void appendOpenHeaderTag(int level, String style) { + if (level > 3 || level < 1) { + return; + } + StringBuffer sb = new StringBuffer("<h"); + sb.append(level); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that a header section ends. + * + * @param level the level of the Header. It should be a value between 1 and 3 + */ + public void appendCloseHeaderTag(int level) { + if (level > 3 || level < 1) { + return; + } + StringBuffer sb = new StringBuffer("</h"); + sb.append(level); + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates an image. + * + * @param align how text should flow around the picture + * @param alt the text to show if you don't show the picture + * @param height how tall is the picture + * @param src where to get the picture + * @param width how wide is the picture + */ + public void appendImageTag(String align, String alt, String height, String src, String width) { + StringBuffer sb = new StringBuffer("<img"); + if (align != null) { + sb.append(" align=\""); + sb.append(align); + sb.append("\""); + } + if (alt != null) { + sb.append(" alt=\""); + sb.append(alt); + sb.append("\""); + } + if (height != null) { + sb.append(" height=\""); + sb.append(height); + sb.append("\""); + } + if (src != null) { + sb.append(" src=\""); + sb.append(src); + sb.append("\""); + } + if (width != null) { + sb.append(" width=\""); + sb.append(width); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates the start of a new line item within a list. + * + * @param style the style of the line item + */ + public void appendLineItemTag(String style) { + StringBuffer sb = new StringBuffer("<li"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that creates an ordered list. "Ordered" means that the order of the items + * in the list is important. To show this, browsers automatically number the list. + * + * @param style the style of the ordered list + */ + public void appendOpenOrderedListTag(String style) { + StringBuffer sb = new StringBuffer("<ol"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that an ordered list section ends. + * + */ + public void appendCloseOrderedListTag() { + text.append("</ol>"); + } + + /** + * Appends a tag that creates an unordered list. The unordered part means that the items + * in the list are not in any particular order. + * + * @param style the style of the unordered list + */ + public void appendOpenUnorderedListTag(String style) { + StringBuffer sb = new StringBuffer("<ul"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that an unordered list section ends. + * + */ + public void appendCloseUnorderedListTag() { + text.append("</ul>"); + } + + /** + * Appends a tag that indicates the start of a new paragraph. This is usually rendered + * with two carriage returns, producing a single blank line in between the two paragraphs. + * + * @param style the style of the paragraph + */ + public void appendOpenParagraphTag(String style) { + StringBuffer sb = new StringBuffer("<p"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates the end of a new paragraph. This is usually rendered + * with two carriage returns, producing a single blank line in between the two paragraphs. + * + */ + public void appendCloseParagraphTag() { + text.append("</p>"); + } + + /** + * Appends a tag that indicates that an inlined quote section begins. + * + * @param style the style of the inlined quote + */ + public void appendOpenInlinedQuoteTag(String style) { + StringBuffer sb = new StringBuffer("<q"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that an inlined quote section ends. + * + */ + public void appendCloseInlinedQuoteTag() { + text.append("</q>"); + } + + /** + * Appends a tag that allows to set the fonts for a span of text. + * + * @param style the style for a span of text + */ + public void appendOpenSpanTag(String style) { + StringBuffer sb = new StringBuffer("<span"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that a span section ends. + * + */ + public void appendCloseSpanTag() { + text.append("</span>"); + } + + /** + * Appends a tag that indicates text which should be more forceful than surrounding text. + * + */ + public void appendOpenStrongTag() { + text.append("<strong>"); + } + + /** + * Appends a tag that indicates that a strong section ends. + * + */ + public void appendCloseStrongTag() { + text.append("</strong>"); + } + + /** + * Appends a given text to the XHTMLText. + * + * @param textToAppend the text to append + */ + public void append(String textToAppend) { + text.append(StringUtils.escapeForXML(textToAppend)); + } + + /** + * Returns the text of the XHTMLText. + * + * Note: Automatically adds the closing body tag. + * + * @return the text of the XHTMLText + */ + public String toString() { + return text.toString().concat(closeBodyTag()); + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebugger.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebugger.java new file mode 100644 index 000000000..3e1a078cd --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebugger.java @@ -0,0 +1,985 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.debugger; + +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.debugger.SmackDebugger; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.util.*; + +import javax.swing.*; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.DefaultTableModel; +import javax.swing.text.BadLocationException; +import javax.xml.transform.*; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * The EnhancedDebugger is a debugger that allows to debug sent, received and interpreted messages + * but also provides the ability to send ad-hoc messages composed by the user.<p> + * <p/> + * A new EnhancedDebugger will be created for each connection to debug. All the EnhancedDebuggers + * will be shown in the same debug window provided by the class EnhancedDebuggerWindow. + * + * @author Gaston Dombiak + */ +public class EnhancedDebugger implements SmackDebugger { + + private static final String NEWLINE = "\n"; + + private static ImageIcon packetReceivedIcon; + private static ImageIcon packetSentIcon; + private static ImageIcon presencePacketIcon; + private static ImageIcon iqPacketIcon; + private static ImageIcon messagePacketIcon; + private static ImageIcon unknownPacketTypeIcon; + + { + URL url; + // Load the image icons + url = + Thread.currentThread().getContextClassLoader().getResource("images/nav_left_blue.png"); + if (url != null) { + packetReceivedIcon = new ImageIcon(url); + } + url = + Thread.currentThread().getContextClassLoader().getResource("images/nav_right_red.png"); + if (url != null) { + packetSentIcon = new ImageIcon(url); + } + url = + Thread.currentThread().getContextClassLoader().getResource("images/photo_portrait.png"); + if (url != null) { + presencePacketIcon = new ImageIcon(url); + } + url = + Thread.currentThread().getContextClassLoader().getResource( + "images/question_and_answer.png"); + if (url != null) { + iqPacketIcon = new ImageIcon(url); + } + url = Thread.currentThread().getContextClassLoader().getResource("images/message.png"); + if (url != null) { + messagePacketIcon = new ImageIcon(url); + } + url = Thread.currentThread().getContextClassLoader().getResource("images/unknown.png"); + if (url != null) { + unknownPacketTypeIcon = new ImageIcon(url); + } + } + + private DefaultTableModel messagesTable = null; + private JTextArea messageTextArea = null; + private JFormattedTextField userField = null; + private JFormattedTextField statusField = null; + + private XMPPConnection connection = null; + + private PacketListener packetReaderListener = null; + private PacketListener packetWriterListener = null; + private ConnectionListener connListener = null; + + private Writer writer; + private Reader reader; + private ReaderListener readerListener; + private WriterListener writerListener; + + private Date creationTime = new Date(); + + // Statistics variables + private DefaultTableModel statisticsTable = null; + private int sentPackets = 0; + private int receivedPackets = 0; + private int sentIQPackets = 0; + private int receivedIQPackets = 0; + private int sentMessagePackets = 0; + private int receivedMessagePackets = 0; + private int sentPresencePackets = 0; + private int receivedPresencePackets = 0; + private int sentOtherPackets = 0; + private int receivedOtherPackets = 0; + + JTabbedPane tabbedPane; + + public EnhancedDebugger(XMPPConnection connection, Writer writer, Reader reader) { + this.connection = connection; + this.writer = writer; + this.reader = reader; + createDebug(); + EnhancedDebuggerWindow.addDebugger(this); + } + + /** + * Creates the debug process, which is a GUI window that displays XML traffic. + */ + private void createDebug() { + // We'll arrange the UI into six tabs. The first tab contains all data, the second + // client generated XML, the third server generated XML, the fourth allows to send + // ad-hoc messages and the fifth contains connection information. + tabbedPane = new JTabbedPane(); + + // Add the All Packets, Sent, Received and Interpreted panels + addBasicPanels(); + + // Add the panel to send ad-hoc messages + addAdhocPacketPanel(); + + // Add the connection information panel + addInformationPanel(); + + // Create a thread that will listen for all incoming packets and write them to + // the GUI. This is what we call "interpreted" packet data, since it's the packet + // data as Smack sees it and not as it's coming in as raw XML. + packetReaderListener = new PacketListener() { + SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm:ss aaa"); + + public void processPacket(final Packet packet) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + addReadPacketToTable(dateFormatter, packet); + } + }); + + } + }; + + // Create a thread that will listen for all outgoing packets and write them to + // the GUI. + packetWriterListener = new PacketListener() { + SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm:ss aaa"); + + public void processPacket(final Packet packet) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + addSentPacketToTable(dateFormatter, packet); + } + }); + + } + }; + + // Create a thread that will listen for any connection closed event + connListener = new ConnectionListener() { + public void connectionClosed() { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + statusField.setValue("Closed"); + EnhancedDebuggerWindow.connectionClosed(EnhancedDebugger.this); + } + }); + + } + + public void connectionClosedOnError(final Exception e) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + statusField.setValue("Closed due to an exception"); + EnhancedDebuggerWindow.connectionClosedOnError(EnhancedDebugger.this, e); + } + }); + + } + }; + } + + private void addBasicPanels() { + JSplitPane allPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + allPane.setOneTouchExpandable(true); + + messagesTable = + new DefaultTableModel( + new Object[]{"Hide", "Timestamp", "", "", "Message", "Id", "Type", "To", "From"}, + 0) { + public boolean isCellEditable(int rowIndex, int mColIndex) { + return false; + } + + public Class getColumnClass(int columnIndex) { + if (columnIndex == 2 || columnIndex == 3) { + return Icon.class; + } + return super.getColumnClass(columnIndex); + } + + }; + JTable table = new JTable(messagesTable); + // Allow only single a selection + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + // Hide the first column + table.getColumnModel().getColumn(0).setMaxWidth(0); + table.getColumnModel().getColumn(0).setMinWidth(0); + table.getTableHeader().getColumnModel().getColumn(0).setMaxWidth(0); + table.getTableHeader().getColumnModel().getColumn(0).setMinWidth(0); + // Set the column "timestamp" size + table.getColumnModel().getColumn(1).setMaxWidth(300); + table.getColumnModel().getColumn(1).setPreferredWidth(70); + // Set the column "direction" icon size + table.getColumnModel().getColumn(2).setMaxWidth(50); + table.getColumnModel().getColumn(2).setPreferredWidth(30); + // Set the column "packet type" icon size + table.getColumnModel().getColumn(3).setMaxWidth(50); + table.getColumnModel().getColumn(3).setPreferredWidth(30); + // Set the column "Id" size + table.getColumnModel().getColumn(5).setMaxWidth(100); + table.getColumnModel().getColumn(5).setPreferredWidth(55); + // Set the column "type" size + table.getColumnModel().getColumn(6).setMaxWidth(200); + table.getColumnModel().getColumn(6).setPreferredWidth(50); + // Set the column "to" size + table.getColumnModel().getColumn(7).setMaxWidth(300); + table.getColumnModel().getColumn(7).setPreferredWidth(90); + // Set the column "from" size + table.getColumnModel().getColumn(8).setMaxWidth(300); + table.getColumnModel().getColumn(8).setPreferredWidth(90); + // Create a table listener that listen for row selection events + SelectionListener selectionListener = new SelectionListener(table); + table.getSelectionModel().addListSelectionListener(selectionListener); + table.getColumnModel().getSelectionModel().addListSelectionListener(selectionListener); + allPane.setTopComponent(new JScrollPane(table)); + messageTextArea = new JTextArea(); + messageTextArea.setEditable(false); + // Add pop-up menu. + JPopupMenu menu = new JPopupMenu(); + JMenuItem menuItem1 = new JMenuItem("Copy"); + menuItem1.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + // Get the clipboard + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + // Set the sent text as the new content of the clipboard + clipboard.setContents(new StringSelection(messageTextArea.getText()), null); + } + }); + menu.add(menuItem1); + // Add listener to the text area so the popup menu can come up. + messageTextArea.addMouseListener(new PopupListener(menu)); + allPane.setBottomComponent(new JScrollPane(messageTextArea)); + allPane.setDividerLocation(150); + + tabbedPane.add("All Packets", allPane); + tabbedPane.setToolTipTextAt(0, "Sent and received packets processed by Smack"); + + // Create UI elements for client generated XML traffic. + final JTextArea sentText = new JTextArea(); + sentText.setWrapStyleWord(true); + sentText.setLineWrap(true); + sentText.setEditable(false); + sentText.setForeground(new Color(112, 3, 3)); + tabbedPane.add("Raw Sent Packets", new JScrollPane(sentText)); + tabbedPane.setToolTipTextAt(1, "Raw text of the sent packets"); + + // Add pop-up menu. + menu = new JPopupMenu(); + menuItem1 = new JMenuItem("Copy"); + menuItem1.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + // Get the clipboard + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + // Set the sent text as the new content of the clipboard + clipboard.setContents(new StringSelection(sentText.getText()), null); + } + }); + + JMenuItem menuItem2 = new JMenuItem("Clear"); + menuItem2.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + sentText.setText(""); + } + }); + + // Add listener to the text area so the popup menu can come up. + sentText.addMouseListener(new PopupListener(menu)); + menu.add(menuItem1); + menu.add(menuItem2); + + // Create UI elements for server generated XML traffic. + final JTextArea receivedText = new JTextArea(); + receivedText.setWrapStyleWord(true); + receivedText.setLineWrap(true); + receivedText.setEditable(false); + receivedText.setForeground(new Color(6, 76, 133)); + tabbedPane.add("Raw Received Packets", new JScrollPane(receivedText)); + tabbedPane.setToolTipTextAt( + 2, + "Raw text of the received packets before Smack process them"); + + // Add pop-up menu. + menu = new JPopupMenu(); + menuItem1 = new JMenuItem("Copy"); + menuItem1.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + // Get the clipboard + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + // Set the sent text as the new content of the clipboard + clipboard.setContents(new StringSelection(receivedText.getText()), null); + } + }); + + menuItem2 = new JMenuItem("Clear"); + menuItem2.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + receivedText.setText(""); + } + }); + + // Add listener to the text area so the popup menu can come up. + receivedText.addMouseListener(new PopupListener(menu)); + menu.add(menuItem1); + menu.add(menuItem2); + + // Create a special Reader that wraps the main Reader and logs data to the GUI. + ObservableReader debugReader = new ObservableReader(reader); + readerListener = new ReaderListener() { + public void read(final String str) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + if (EnhancedDebuggerWindow.PERSISTED_DEBUGGER && + !EnhancedDebuggerWindow.getInstance().isVisible()) { + // Do not add content if the parent is not visible + return; + } + + int index = str.lastIndexOf(">"); + if (index != -1) { + if (receivedText.getLineCount() >= EnhancedDebuggerWindow.MAX_TABLE_ROWS) + { + try { + receivedText.replaceRange("", 0, receivedText.getLineEndOffset(0)); + } + catch (BadLocationException e) { + e.printStackTrace(); + } + } + receivedText.append(str.substring(0, index + 1)); + receivedText.append(NEWLINE); + if (str.length() > index) { + receivedText.append(str.substring(index + 1)); + } + } + else { + receivedText.append(str); + } + } + }); + } + }; + debugReader.addReaderListener(readerListener); + + // Create a special Writer that wraps the main Writer and logs data to the GUI. + ObservableWriter debugWriter = new ObservableWriter(writer); + writerListener = new WriterListener() { + public void write(final String str) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + if (EnhancedDebuggerWindow.PERSISTED_DEBUGGER && + !EnhancedDebuggerWindow.getInstance().isVisible()) { + // Do not add content if the parent is not visible + return; + } + + if (sentText.getLineCount() >= EnhancedDebuggerWindow.MAX_TABLE_ROWS) { + try { + sentText.replaceRange("", 0, sentText.getLineEndOffset(0)); + } + catch (BadLocationException e) { + e.printStackTrace(); + } + } + + sentText.append(str); + if (str.endsWith(">")) { + sentText.append(NEWLINE); + } + } + }); + + + } + }; + debugWriter.addWriterListener(writerListener); + + // Assign the reader/writer objects to use the debug versions. The packet reader + // and writer will use the debug versions when they are created. + reader = debugReader; + writer = debugWriter; + + } + + private void addAdhocPacketPanel() { + // Create UI elements for sending ad-hoc messages. + final JTextArea adhocMessages = new JTextArea(); + adhocMessages.setEditable(true); + adhocMessages.setForeground(new Color(1, 94, 35)); + tabbedPane.add("Ad-hoc message", new JScrollPane(adhocMessages)); + tabbedPane.setToolTipTextAt(3, "Panel that allows you to send adhoc packets"); + + // Add pop-up menu. + JPopupMenu menu = new JPopupMenu(); + JMenuItem menuItem = new JMenuItem("Message"); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + adhocMessages.setText( + "<message to=\"\" id=\"" + + StringUtils.randomString(5) + + "-X\"><body></body></message>"); + } + }); + menu.add(menuItem); + + menuItem = new JMenuItem("IQ Get"); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + adhocMessages.setText( + "<iq type=\"get\" to=\"\" id=\"" + + StringUtils.randomString(5) + + "-X\"><query xmlns=\"\"></query></iq>"); + } + }); + menu.add(menuItem); + + menuItem = new JMenuItem("IQ Set"); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + adhocMessages.setText( + "<iq type=\"set\" to=\"\" id=\"" + + StringUtils.randomString(5) + + "-X\"><query xmlns=\"\"></query></iq>"); + } + }); + menu.add(menuItem); + + menuItem = new JMenuItem("Presence"); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + adhocMessages.setText( + "<presence to=\"\" id=\"" + StringUtils.randomString(5) + "-X\"/>"); + } + }); + menu.add(menuItem); + menu.addSeparator(); + + menuItem = new JMenuItem("Send"); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + if (!"".equals(adhocMessages.getText())) { + AdHocPacket packetToSend = new AdHocPacket(adhocMessages.getText()); + connection.sendPacket(packetToSend); + } + } + }); + menu.add(menuItem); + + menuItem = new JMenuItem("Clear"); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + adhocMessages.setText(null); + } + }); + menu.add(menuItem); + + // Add listener to the text area so the popup menu can come up. + adhocMessages.addMouseListener(new PopupListener(menu)); + } + + private void addInformationPanel() { + // Create UI elements for connection information. + JPanel informationPanel = new JPanel(); + informationPanel.setLayout(new BorderLayout()); + + // Add the Host information + JPanel connPanel = new JPanel(); + connPanel.setLayout(new GridBagLayout()); + connPanel.setBorder(BorderFactory.createTitledBorder("Connection information")); + + JLabel label = new JLabel("Host: "); + label.setMinimumSize(new java.awt.Dimension(150, 14)); + label.setMaximumSize(new java.awt.Dimension(150, 14)); + connPanel.add( + label, + new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0)); + JFormattedTextField field = new JFormattedTextField(connection.getServiceName()); + field.setMinimumSize(new java.awt.Dimension(150, 20)); + field.setMaximumSize(new java.awt.Dimension(150, 20)); + field.setEditable(false); + field.setBorder(null); + connPanel.add( + field, + new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0)); + + // Add the Port information + label = new JLabel("Port: "); + label.setMinimumSize(new java.awt.Dimension(150, 14)); + label.setMaximumSize(new java.awt.Dimension(150, 14)); + connPanel.add( + label, + new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0)); + field = new JFormattedTextField(new Integer(connection.getPort())); + field.setMinimumSize(new java.awt.Dimension(150, 20)); + field.setMaximumSize(new java.awt.Dimension(150, 20)); + field.setEditable(false); + field.setBorder(null); + connPanel.add( + field, + new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0)); + + // Add the connection's User information + label = new JLabel("User: "); + label.setMinimumSize(new java.awt.Dimension(150, 14)); + label.setMaximumSize(new java.awt.Dimension(150, 14)); + connPanel.add( + label, + new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0)); + userField = new JFormattedTextField(); + userField.setMinimumSize(new java.awt.Dimension(150, 20)); + userField.setMaximumSize(new java.awt.Dimension(150, 20)); + userField.setEditable(false); + userField.setBorder(null); + connPanel.add( + userField, + new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0)); + + // Add the connection's creationTime information + label = new JLabel("Creation time: "); + label.setMinimumSize(new java.awt.Dimension(150, 14)); + label.setMaximumSize(new java.awt.Dimension(150, 14)); + connPanel.add( + label, + new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0)); + field = new JFormattedTextField(new SimpleDateFormat("yyyy.MM.dd hh:mm:ss aaa")); + field.setMinimumSize(new java.awt.Dimension(150, 20)); + field.setMaximumSize(new java.awt.Dimension(150, 20)); + field.setValue(creationTime); + field.setEditable(false); + field.setBorder(null); + connPanel.add( + field, + new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0)); + + // Add the connection's creationTime information + label = new JLabel("Status: "); + label.setMinimumSize(new java.awt.Dimension(150, 14)); + label.setMaximumSize(new java.awt.Dimension(150, 14)); + connPanel.add( + label, + new GridBagConstraints(0, 4, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0)); + statusField = new JFormattedTextField(); + statusField.setMinimumSize(new java.awt.Dimension(150, 20)); + statusField.setMaximumSize(new java.awt.Dimension(150, 20)); + statusField.setValue("Active"); + statusField.setEditable(false); + statusField.setBorder(null); + connPanel.add( + statusField, + new GridBagConstraints(1, 4, 1, 1, 0.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0)); + // Add the connection panel to the information panel + informationPanel.add(connPanel, BorderLayout.NORTH); + + // Add the Number of sent packets information + JPanel packetsPanel = new JPanel(); + packetsPanel.setLayout(new GridLayout(1, 1)); + packetsPanel.setBorder(BorderFactory.createTitledBorder("Transmitted Packets")); + + statisticsTable = + new DefaultTableModel(new Object[][]{{"IQ", new Integer(0), new Integer(0)}, { + "Message", new Integer(0), new Integer(0) + }, { + "Presence", new Integer(0), new Integer(0) + }, { + "Other", new Integer(0), new Integer(0) + }, { + "Total", new Integer(0), new Integer(0) + } + }, new Object[]{"Type", "Received", "Sent"}) { + public boolean isCellEditable(int rowIndex, int mColIndex) { + return false; + } + }; + JTable table = new JTable(statisticsTable); + // Allow only single a selection + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + packetsPanel.add(new JScrollPane(table)); + + // Add the packets panel to the information panel + informationPanel.add(packetsPanel, BorderLayout.CENTER); + + tabbedPane.add("Information", new JScrollPane(informationPanel)); + tabbedPane.setToolTipTextAt(4, "Information and statistics about the debugged connection"); + } + + public Reader newConnectionReader(Reader newReader) { + ((ObservableReader) reader).removeReaderListener(readerListener); + ObservableReader debugReader = new ObservableReader(newReader); + debugReader.addReaderListener(readerListener); + reader = debugReader; + return reader; + } + + public Writer newConnectionWriter(Writer newWriter) { + ((ObservableWriter) writer).removeWriterListener(writerListener); + ObservableWriter debugWriter = new ObservableWriter(newWriter); + debugWriter.addWriterListener(writerListener); + writer = debugWriter; + return writer; + } + + public void userHasLogged(final String user) { + final EnhancedDebugger debugger = this; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + userField.setText(user); + EnhancedDebuggerWindow.userHasLogged(debugger, user); + // Add the connection listener to the connection so that the debugger can be notified + // whenever the connection is closed. + connection.addConnectionListener(connListener); + } + }); + + } + + public Reader getReader() { + return reader; + } + + public Writer getWriter() { + return writer; + } + + public PacketListener getReaderListener() { + return packetReaderListener; + } + + public PacketListener getWriterListener() { + return packetWriterListener; + } + + /** + * Updates the statistics table + */ + private void updateStatistics() { + statisticsTable.setValueAt(new Integer(receivedIQPackets), 0, 1); + statisticsTable.setValueAt(new Integer(sentIQPackets), 0, 2); + + statisticsTable.setValueAt(new Integer(receivedMessagePackets), 1, 1); + statisticsTable.setValueAt(new Integer(sentMessagePackets), 1, 2); + + statisticsTable.setValueAt(new Integer(receivedPresencePackets), 2, 1); + statisticsTable.setValueAt(new Integer(sentPresencePackets), 2, 2); + + statisticsTable.setValueAt(new Integer(receivedOtherPackets), 3, 1); + statisticsTable.setValueAt(new Integer(sentOtherPackets), 3, 2); + + statisticsTable.setValueAt(new Integer(receivedPackets), 4, 1); + statisticsTable.setValueAt(new Integer(sentPackets), 4, 2); + } + + /** + * Adds the received packet detail to the messages table. + * + * @param dateFormatter the SimpleDateFormat to use to format Dates + * @param packet the read packet to add to the table + */ + private void addReadPacketToTable(final SimpleDateFormat dateFormatter, final Packet packet) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + String messageType = null; + String from = packet.getFrom(); + String type = ""; + Icon packetTypeIcon; + receivedPackets++; + if (packet instanceof IQ) { + packetTypeIcon = iqPacketIcon; + messageType = "IQ Received (class=" + packet.getClass().getName() + ")"; + type = ((IQ) packet).getType().toString(); + receivedIQPackets++; + } + else if (packet instanceof Message) { + packetTypeIcon = messagePacketIcon; + messageType = "Message Received"; + type = ((Message) packet).getType().toString(); + receivedMessagePackets++; + } + else if (packet instanceof Presence) { + packetTypeIcon = presencePacketIcon; + messageType = "Presence Received"; + type = ((Presence) packet).getType().toString(); + receivedPresencePackets++; + } + else { + packetTypeIcon = unknownPacketTypeIcon; + messageType = packet.getClass().getName() + " Received"; + receivedOtherPackets++; + } + + // Check if we need to remove old rows from the table to keep memory consumption low + if (EnhancedDebuggerWindow.MAX_TABLE_ROWS > 0 && + messagesTable.getRowCount() >= EnhancedDebuggerWindow.MAX_TABLE_ROWS) { + messagesTable.removeRow(0); + } + + messagesTable.addRow( + new Object[]{ + formatXML(packet.toXML()), + dateFormatter.format(new Date()), + packetReceivedIcon, + packetTypeIcon, + messageType, + packet.getPacketID(), + type, + "", + from}); + // Update the statistics table + updateStatistics(); + } + }); + } + + /** + * Adds the sent packet detail to the messages table. + * + * @param dateFormatter the SimpleDateFormat to use to format Dates + * @param packet the sent packet to add to the table + */ + private void addSentPacketToTable(final SimpleDateFormat dateFormatter, final Packet packet) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + String messageType = null; + String to = packet.getTo(); + String type = ""; + Icon packetTypeIcon; + sentPackets++; + if (packet instanceof IQ) { + packetTypeIcon = iqPacketIcon; + messageType = "IQ Sent (class=" + packet.getClass().getName() + ")"; + type = ((IQ) packet).getType().toString(); + sentIQPackets++; + } + else if (packet instanceof Message) { + packetTypeIcon = messagePacketIcon; + messageType = "Message Sent"; + type = ((Message) packet).getType().toString(); + sentMessagePackets++; + } + else if (packet instanceof Presence) { + packetTypeIcon = presencePacketIcon; + messageType = "Presence Sent"; + type = ((Presence) packet).getType().toString(); + sentPresencePackets++; + } + else { + packetTypeIcon = unknownPacketTypeIcon; + messageType = packet.getClass().getName() + " Sent"; + sentOtherPackets++; + } + + // Check if we need to remove old rows from the table to keep memory consumption low + if (EnhancedDebuggerWindow.MAX_TABLE_ROWS > 0 && + messagesTable.getRowCount() >= EnhancedDebuggerWindow.MAX_TABLE_ROWS) { + messagesTable.removeRow(0); + } + + messagesTable.addRow( + new Object[]{ + formatXML(packet.toXML()), + dateFormatter.format(new Date()), + packetSentIcon, + packetTypeIcon, + messageType, + packet.getPacketID(), + type, + to, + ""}); + + // Update the statistics table + updateStatistics(); + } + }); + } + + private String formatXML(String str) { + try { + // Use a Transformer for output + TransformerFactory tFactory = TransformerFactory.newInstance(); + // Surround this setting in a try/catch for compatibility with Java 1.4. This setting is required + // for Java 1.5 + try { + tFactory.setAttribute("indent-number", new Integer(2)); + } + catch (IllegalArgumentException e) { + } + Transformer transformer = tFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + + // Transform the requested string into a nice formatted XML string + StreamSource source = new StreamSource(new StringReader(str)); + StringWriter sw = new StringWriter(); + StreamResult result = new StreamResult(sw); + transformer.transform(source, result); + return sw.toString(); + + } + catch (TransformerConfigurationException tce) { + // Error generated by the parser + System.out.println("\n** Transformer Factory error"); + System.out.println(" " + tce.getMessage()); + + // Use the contained exception, if any + Throwable x = tce; + if (tce.getException() != null) + x = tce.getException(); + x.printStackTrace(); + + } + catch (TransformerException te) { + // Error generated by the parser + System.out.println("\n** Transformation error"); + System.out.println(" " + te.getMessage()); + + // Use the contained exception, if any + Throwable x = te; + if (te.getException() != null) + x = te.getException(); + x.printStackTrace(); + + } + return str; + } + + /** + * Returns true if the debugger's connection with the server is up and running. + * + * @return true if the connection with the server is active. + */ + boolean isConnectionActive() { + return connection.isConnected(); + } + + /** + * Stops debugging the connection. Removes any listener on the connection. + */ + void cancel() { + connection.removeConnectionListener(connListener); + connection.removePacketListener(packetReaderListener); + connection.removePacketWriterListener(packetWriterListener); + ((ObservableReader) reader).removeReaderListener(readerListener); + ((ObservableWriter) writer).removeWriterListener(writerListener); + messagesTable = null; + } + + /** + * An ad-hoc packet is like any regular packet but with the exception that it's intention is + * to be used only <b>to send packets</b>.<p> + * <p/> + * The whole text to send must be passed to the constructor. This implies that the client of + * this class is responsible for sending a valid text to the constructor. + */ + private class AdHocPacket extends Packet { + + private String text; + + /** + * Create a new AdHocPacket with the text to send. The passed text must be a valid text to + * send to the server, no validation will be done on the passed text. + * + * @param text the whole text of the packet to send + */ + public AdHocPacket(String text) { + this.text = text; + } + + public String toXML() { + return text; + } + + } + + /** + * Listens for debug window popup dialog events. + */ + private class PopupListener extends MouseAdapter { + + JPopupMenu popup; + + PopupListener(JPopupMenu popupMenu) { + popup = popupMenu; + } + + public void mousePressed(MouseEvent e) { + maybeShowPopup(e); + } + + public void mouseReleased(MouseEvent e) { + maybeShowPopup(e); + } + + private void maybeShowPopup(MouseEvent e) { + if (e.isPopupTrigger()) { + popup.show(e.getComponent(), e.getX(), e.getY()); + } + } + } + + private class SelectionListener implements ListSelectionListener { + + JTable table; + + // It is necessary to keep the table since it is not possible + // to determine the table from the event's source + SelectionListener(JTable table) { + this.table = table; + } + + public void valueChanged(ListSelectionEvent e) { + if (table.getSelectedRow() == -1) { + // Clear the messageTextArea since there is none packet selected + messageTextArea.setText(null); + } + else { + // Set the detail of the packet in the messageTextArea + messageTextArea.setText( + (String) table.getModel().getValueAt(table.getSelectedRow(), 0)); + // Scroll up to the top + messageTextArea.setCaretPosition(0); + } + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java new file mode 100644 index 000000000..d47a062f0 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/EnhancedDebuggerWindow.java @@ -0,0 +1,377 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.debugger; + +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.provider.ProviderManager; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.Vector; + +/** + * The EnhancedDebuggerWindow is the main debug window that will show all the EnhancedDebuggers. + * For each connection to debug there will be an EnhancedDebugger that will be shown in the + * EnhancedDebuggerWindow.<p> + * <p/> + * This class also provides information about Smack like for example the Smack version and the + * installed providers. + * + * @author Gaston Dombiak + */ +public class EnhancedDebuggerWindow { + + private static EnhancedDebuggerWindow instance; + + private static ImageIcon connectionCreatedIcon; + private static ImageIcon connectionActiveIcon; + private static ImageIcon connectionClosedIcon; + private static ImageIcon connectionClosedOnErrorIcon; + + public static boolean PERSISTED_DEBUGGER = false; + /** + * Keeps the max number of rows to keep in the tables. A value less than 0 means that packets + * will never be removed. If you are planning to use this debugger in a + * production environment then you should set a lower value (e.g. 50) to prevent the debugger + * from consuming all the JVM memory. + */ + public static int MAX_TABLE_ROWS = 150; + + { + URL url; + + url = + Thread.currentThread().getContextClassLoader().getResource( + "images/trafficlight_off.png"); + if (url != null) { + connectionCreatedIcon = new ImageIcon(url); + } + url = + Thread.currentThread().getContextClassLoader().getResource( + "images/trafficlight_green.png"); + if (url != null) { + connectionActiveIcon = new ImageIcon(url); + } + url = + Thread.currentThread().getContextClassLoader().getResource( + "images/trafficlight_red.png"); + if (url != null) { + connectionClosedIcon = new ImageIcon(url); + } + url = Thread.currentThread().getContextClassLoader().getResource("images/warning.png"); + if (url != null) { + connectionClosedOnErrorIcon = new ImageIcon(url); + } + + } + + private JFrame frame = null; + private JTabbedPane tabbedPane = null; + private java.util.List debuggers = new ArrayList(); + + private EnhancedDebuggerWindow() { + } + + /** + * Returns the unique EnhancedDebuggerWindow instance available in the system. + * + * @return the unique EnhancedDebuggerWindow instance + */ + public static EnhancedDebuggerWindow getInstance() { + if (instance == null) { + instance = new EnhancedDebuggerWindow(); + } + return instance; + } + + /** + * Adds the new specified debugger to the list of debuggers to show in the main window. + * + * @param debugger the new debugger to show in the debug window + */ + synchronized static void addDebugger(EnhancedDebugger debugger) { + getInstance().showNewDebugger(debugger); + } + + /** + * Shows the new debugger in the debug window. + * + * @param debugger the new debugger to show + */ + private void showNewDebugger(EnhancedDebugger debugger) { + if (frame == null) { + createDebug(); + } + debugger.tabbedPane.setName("Connection_" + tabbedPane.getComponentCount()); + tabbedPane.add(debugger.tabbedPane, tabbedPane.getComponentCount() - 1); + tabbedPane.setIconAt(tabbedPane.indexOfComponent(debugger.tabbedPane), connectionCreatedIcon); + frame.setTitle( + "Smack Debug Window -- Total connections: " + (tabbedPane.getComponentCount() - 1)); + // Keep the added debugger for later access + debuggers.add(debugger); + } + + /** + * Notification that a user has logged in to the server. A new title will be set + * to the tab of the given debugger. + * + * @param debugger the debugger whose connection logged in to the server + * @param user the user@host/resource that has just logged in + */ + synchronized static void userHasLogged(EnhancedDebugger debugger, String user) { + int index = getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane); + getInstance().tabbedPane.setTitleAt( + index, + user); + getInstance().tabbedPane.setIconAt( + index, + connectionActiveIcon); + } + + /** + * Notification that the connection was properly closed. + * + * @param debugger the debugger whose connection was properly closed. + */ + synchronized static void connectionClosed(EnhancedDebugger debugger) { + getInstance().tabbedPane.setIconAt( + getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane), + connectionClosedIcon); + } + + /** + * Notification that the connection was closed due to an exception. + * + * @param debugger the debugger whose connection was closed due to an exception. + * @param e the exception. + */ + synchronized static void connectionClosedOnError(EnhancedDebugger debugger, Exception e) { + int index = getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane); + getInstance().tabbedPane.setToolTipTextAt( + index, + "Connection closed due to the exception: " + e.getMessage()); + getInstance().tabbedPane.setIconAt( + index, + connectionClosedOnErrorIcon); + } + + /** + * Creates the main debug window that provides information about Smack and also shows + * a tab panel for each connection that is being debugged. + */ + private void createDebug() { + + frame = new JFrame("Smack Debug Window"); + + if (!PERSISTED_DEBUGGER) { + // Add listener for window closing event + frame.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent evt) { + rootWindowClosing(evt); + } + }); + } + + // We'll arrange the UI into tabs. The last tab contains Smack's information. + // All the connection debugger tabs will be shown before the Smack info tab. + tabbedPane = new JTabbedPane(); + + // Create the Smack info panel + JPanel informationPanel = new JPanel(); + informationPanel.setLayout(new BoxLayout(informationPanel, BoxLayout.Y_AXIS)); + + // Add the Smack version label + JPanel versionPanel = new JPanel(); + versionPanel.setLayout(new BoxLayout(versionPanel, BoxLayout.X_AXIS)); + versionPanel.setMaximumSize(new Dimension(2000, 31)); + versionPanel.add(new JLabel(" Smack version: ")); + JFormattedTextField field = new JFormattedTextField(SmackConfiguration.getVersion()); + field.setEditable(false); + field.setBorder(null); + versionPanel.add(field); + informationPanel.add(versionPanel); + + // Add the list of installed IQ Providers + JPanel iqProvidersPanel = new JPanel(); + iqProvidersPanel.setLayout(new GridLayout(1, 1)); + iqProvidersPanel.setBorder(BorderFactory.createTitledBorder("Installed IQ Providers")); + Vector providers = new Vector(); + for (Iterator it = ProviderManager.getDefault().getIQProviders(); it.hasNext();) { + Object provider = it.next(); + if (provider.getClass() == Class.class) { + providers.add(((Class) provider).getName()); + } + else { + providers.add(provider.getClass().getName()); + } + } + // Sort the collection of providers + Collections.sort(providers); + JList list = new JList(providers); + iqProvidersPanel.add(new JScrollPane(list)); + informationPanel.add(iqProvidersPanel); + + // Add the list of installed Extension Providers + JPanel extensionProvidersPanel = new JPanel(); + extensionProvidersPanel.setLayout(new GridLayout(1, 1)); + extensionProvidersPanel.setBorder(BorderFactory.createTitledBorder("Installed Extension Providers")); + providers = new Vector(); + for (Iterator it = ProviderManager.getDefault().getExtensionProviders(); it.hasNext();) { + Object provider = it.next(); + if (provider.getClass() == Class.class) { + providers.add(((Class) provider).getName()); + } + else { + providers.add(provider.getClass().getName()); + } + } + // Sort the collection of providers + Collections.sort(providers); + list = new JList(providers); + extensionProvidersPanel.add(new JScrollPane(list)); + informationPanel.add(extensionProvidersPanel); + + tabbedPane.add("Smack Info", informationPanel); + + // Add pop-up menu. + JPopupMenu menu = new JPopupMenu(); + // Add a menu item that allows to close the current selected tab + JMenuItem menuItem = new JMenuItem("Close"); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + // Remove the selected tab pane if it's not the Smack info pane + if (tabbedPane.getSelectedIndex() < tabbedPane.getComponentCount() - 1) { + int index = tabbedPane.getSelectedIndex(); + // Notify to the debugger to stop debugging + EnhancedDebugger debugger = (EnhancedDebugger) debuggers.get(index); + debugger.cancel(); + // Remove the debugger from the root window + tabbedPane.remove(debugger.tabbedPane); + debuggers.remove(debugger); + // Update the root window title + frame.setTitle( + "Smack Debug Window -- Total connections: " + + (tabbedPane.getComponentCount() - 1)); + } + } + }); + menu.add(menuItem); + // Add a menu item that allows to close all the tabs that have their connections closed + menuItem = new JMenuItem("Close All Not Active"); + menuItem.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + ArrayList debuggersToRemove = new ArrayList(); + // Remove all the debuggers of which their connections are no longer valid + for (int index = 0; index < tabbedPane.getComponentCount() - 1; index++) { + EnhancedDebugger debugger = (EnhancedDebugger) debuggers.get(index); + if (!debugger.isConnectionActive()) { + // Notify to the debugger to stop debugging + debugger.cancel(); + debuggersToRemove.add(debugger); + } + } + for (Iterator it = debuggersToRemove.iterator(); it.hasNext();) { + EnhancedDebugger debugger = (EnhancedDebugger) it.next(); + // Remove the debugger from the root window + tabbedPane.remove(debugger.tabbedPane); + debuggers.remove(debugger); + } + // Update the root window title + frame.setTitle( + "Smack Debug Window -- Total connections: " + + (tabbedPane.getComponentCount() - 1)); + } + }); + menu.add(menuItem); + // Add listener to the text area so the popup menu can come up. + tabbedPane.addMouseListener(new PopupListener(menu)); + + frame.getContentPane().add(tabbedPane); + + frame.setSize(650, 400); + + if (!PERSISTED_DEBUGGER) { + frame.setVisible(true); + } + } + + /** + * Notification that the root window is closing. Stop listening for received and + * transmitted packets in all the debugged connections. + * + * @param evt the event that indicates that the root window is closing + */ + public void rootWindowClosing(WindowEvent evt) { + // Notify to all the debuggers to stop debugging + for (Iterator it = debuggers.iterator(); it.hasNext();) { + EnhancedDebugger debugger = (EnhancedDebugger) it.next(); + debugger.cancel(); + } + // Release any reference to the debuggers + debuggers.removeAll(debuggers); + // Release the default instance + instance = null; + } + + /** + * Listens for debug window popup dialog events. + */ + private class PopupListener extends MouseAdapter { + + JPopupMenu popup; + + PopupListener(JPopupMenu popupMenu) { + popup = popupMenu; + } + + public void mousePressed(MouseEvent e) { + maybeShowPopup(e); + } + + public void mouseReleased(MouseEvent e) { + maybeShowPopup(e); + } + + private void maybeShowPopup(MouseEvent e) { + if (e.isPopupTrigger()) { + popup.show(e.getComponent(), e.getX(), e.getY()); + } + } + } + + public void setVisible(boolean visible) { + if (frame != null) { + frame.setVisible(visible); + } + } + + public boolean isVisible() { + if (frame != null) { + return frame.isVisible(); + } + return false; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/package.html new file mode 100644 index 000000000..8ea20e0ac --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/debugger/package.html @@ -0,0 +1 @@ +<body>Smack optional Debuggers.</body>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Base64.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Base64.java new file mode 100644 index 000000000..8a7cf59a4 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Base64.java @@ -0,0 +1,1416 @@ +package org.jivesoftware.smackx.filetransfer; + +/** + * Encodes and decodes to and from Base64 notation. <p/> <p/> Change Log: + * </p> + * <ul> + * <li>v2.1 - Cleaned up javadoc comments and unused variables and methods. + * Added some convenience methods for reading and writing to and from files.</li> + * <li>v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on + * systems with other encodings (like EBCDIC).</li> + * <li>v2.0.1 - Fixed an error when decoding a single byte, that is, when the + * encoded data was a single byte.</li> + * <li>v2.0 - I got rid of methods that used booleans to set options. Now + * everything is more consolidated and cleaner. The code now detects when data + * that's being decoded is gzip-compressed and will decompress it automatically. + * Generally things are cleaner. You'll probably have to change some method + * calls that you were making to support the new options format (<tt>int</tt>s + * that you "OR" together).</li> + * <li>v1.5.1 - Fixed bug when decompressing and decoding to a byte[] using + * <tt>decode( String s, boolean gzipCompressed )</tt>. Added the ability to + * "suspend" encoding in the Output Stream so you can turn on and off the + * encoding if you need to embed base64 data in an otherwise "normal" stream + * (like an XML file).</li> + * <li>v1.5 - Output stream pases on flush() command but doesn't do anything + * itself. This helps when using GZIP streams. Added the ability to + * GZip-compress objects before encoding them.</li> + * <li>v1.4 - Added helper methods to read/write files.</li> + * <li>v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.</li> + * <li>v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input + * stream where last buffer being read, if not completely full, was not + * returned.</li> + * <li>v1.3.4 - Fixed when "improperly padded stream" error was thrown at the + * wrong time.</li> + * <li>v1.3.3 - Fixed I/O streams which were totally messed up.</li> + * </ul> + * <p/> <p/> I am placing this code in the Public Domain. Do with it as you + * will. This software comes with no guarantees or warranties but with plenty of + * well-wishing instead! Please visit <a + * href="http://iharder.net/base64">http://iharder.net/base64</a> periodically + * to check for updates or to contribute improvements. + * </p> + * + * @author Robert Harder + * @author rob@iharder.net + * @version 2.1 + */ +class Base64 { + + /* ******** P U B L I C F I E L D S ******** */ + + /** + * No options specified. Value is zero. + */ + public final static int NO_OPTIONS = 0; + + /** + * Specify encoding. + */ + public final static int ENCODE = 1; + + /** + * Specify decoding. + */ + public final static int DECODE = 0; + + /** + * Specify that data should be gzip-compressed. + */ + public final static int GZIP = 2; + + /** + * Don't break lines when encoding (violates strict Base64 specification) + */ + public final static int DONT_BREAK_LINES = 8; + + /* ******** P R I V A T E F I E L D S ******** */ + + /** + * Maximum line length (76) of Base64 output. + */ + private final static int MAX_LINE_LENGTH = 76; + + /** + * The equals sign (=) as a byte. + */ + private final static byte EQUALS_SIGN = (byte) '='; + + /** + * The new line character (\n) as a byte. + */ + private final static byte NEW_LINE = (byte) '\n'; + + /** + * Preferred encoding. + */ + private final static String PREFERRED_ENCODING = "UTF-8"; + + /** + * The 64 valid Base64 values. + */ + private final static byte[] ALPHABET; + + private final static byte[] _NATIVE_ALPHABET = /* + * May be something funny + * like EBCDIC + */ + { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', + (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', + (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', + (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', + (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', + (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', + (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', + (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', + (byte) '9', (byte) '+', (byte) '/' }; + + /** Determine which ALPHABET to use. */ + static { + byte[] __bytes; + try { + __bytes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + .getBytes(PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException use) { + __bytes = _NATIVE_ALPHABET; // Fall back to native encoding + } // end catch + ALPHABET = __bytes; + } // end static + + /** + * Translates a Base64 value to either its 6-bit reconstruction value or a + * negative number indicating some other meaning. + */ + private final static byte[] DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, + -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - + // 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, -9, -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' + // through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' + // through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' + // through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' + // through 'z' + -9, -9, -9, -9 // Decimal 123 - 126 + /* + * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 + */ + }; + + // I think I end up not using the BAD_ENCODING indicator. + // private final static byte BAD_ENCODING = -9; // Indicates error in + // encoding + private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in + // encoding + + private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in + // encoding + + /** + * Defeats instantiation. + */ + private Base64() { + } + + /* ******** E N C O D I N G M E T H O D S ******** */ + + /** + * Encodes up to the first three bytes of array <var>threeBytes</var> and + * returns a four-byte array in Base64 notation. The actual number of + * significant bytes in your array is given by <var>numSigBytes</var>. The + * array <var>threeBytes</var> needs only be as big as <var>numSigBytes</var>. + * Code can reuse a byte array by passing a four-byte array as <var>b4</var>. + * + * @param b4 + * A reusable byte array to reduce array instantiation + * @param threeBytes + * the array to convert + * @param numSigBytes + * the number of significant bytes in your array + * @return four byte array in Base64 notation. + * @since 1.5.1 + */ + private static byte[] encode3to4(byte[] b4, byte[] threeBytes, + int numSigBytes) { + encode3to4(threeBytes, 0, numSigBytes, b4, 0); + return b4; + } // end encode3to4 + + /** + * Encodes up to three bytes of the array <var>source</var> and writes the + * resulting four Base64 bytes to <var>destination</var>. The source and + * destination arrays can be manipulated anywhere along their length by + * specifying <var>srcOffset</var> and <var>destOffset</var>. This method + * does not check to make sure your arrays are large enough to accomodate + * <var>srcOffset</var> + 3 for the <var>source</var> array or + * <var>destOffset</var> + 4 for the <var>destination</var> array. The + * actual number of significant bytes in your array is given by + * <var>numSigBytes</var>. + * + * @param source + * the array to convert + * @param srcOffset + * the index where conversion begins + * @param numSigBytes + * the number of significant bytes in your array + * @param destination + * the array to hold the conversion + * @param destOffset + * the index where output will be put + * @return the <var>destination</var> array + * @since 1.3 + */ + private static byte[] encode3to4(byte[] source, int srcOffset, + int numSigBytes, byte[] destination, int destOffset) { + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an + // int. + int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f]; + return destination; + + case 2: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + case 1: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Serializes an object and returns the Base64-encoded version of that + * serialized object. If the object cannot be serialized or there is another + * error, the method will return <tt>null</tt>. The object is not + * GZip-compressed before being encoded. + * + * @param serializableObject + * The object to encode + * @return The Base64-encoded object + * @since 1.4 + */ + public static String encodeObject(java.io.Serializable serializableObject) { + return encodeObject(serializableObject, NO_OPTIONS); + } // end encodeObject + + /** + * Serializes an object and returns the Base64-encoded version of that + * serialized object. If the object cannot be serialized or there is another + * error, the method will return <tt>null</tt>. <p/> Valid options: + * + * <pre> + * GZIP: gzip-compresses object before encoding it. + * DONT_BREAK_LINES: don't break lines at 76 characters + * <i>Note: Technically, this makes your encoding non-compliant.</i> + * </pre> + * + * <p/> Example: <code>encodeObject( myObj, Base64.GZIP )</code> or <p/> + * Example: + * <code>encodeObject( myObj, Base64.GZIP | Base64.DONT_BREAK_LINES )</code> + * + * @param serializableObject + * The object to encode + * @param options + * Specified options + * @return The Base64-encoded object + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeObject(java.io.Serializable serializableObject, + int options) { + // Streams + java.io.ByteArrayOutputStream baos = null; + java.io.OutputStream b64os = null; + java.io.ObjectOutputStream oos = null; + java.util.zip.GZIPOutputStream gzos = null; + + // Isolate options + int gzip = (options & GZIP); + int dontBreakLines = (options & DONT_BREAK_LINES); + + try { + // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | dontBreakLines); + + // GZip? + if (gzip == GZIP) { + gzos = new java.util.zip.GZIPOutputStream(b64os); + oos = new java.io.ObjectOutputStream(gzos); + } // end if: gzip + else { + oos = new java.io.ObjectOutputStream(b64os); + } + + oos.writeObject(serializableObject); + } // end try + catch (java.io.IOException e) { + e.printStackTrace(); + return null; + } // end catch + finally { + try { + oos.close(); + } catch (Exception e) { + } + try { + gzos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + // Return value according to relevant encoding. + try { + return new String(baos.toByteArray(), PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(baos.toByteArray()); + } // end catch + + } // end encode + + /** + * Encodes a byte array into Base64 notation. Does not GZip-compress data. + * + * @param source + * The data to convert + * @since 1.4 + */ + public static String encodeBytes(byte[] source) { + return encodeBytes(source, 0, source.length, NO_OPTIONS); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation. <p/> Valid options: + * + * <pre> + * GZIP: gzip-compresses object before encoding it. + * DONT_BREAK_LINES: don't break lines at 76 characters + * <i>Note: Technically, this makes your encoding non-compliant.</i> + * </pre> + * + * <p/> Example: <code>encodeBytes( myData, Base64.GZIP )</code> or <p/> + * Example: + * <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code> + * + * @param source + * The data to convert + * @param options + * Specified options + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int options) { + return encodeBytes(source, 0, source.length, options); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation. Does not GZip-compress data. + * + * @param source + * The data to convert + * @param off + * Offset in array where conversion should begin + * @param len + * Length of data to convert + * @since 1.4 + */ + public static String encodeBytes(byte[] source, int off, int len) { + return encodeBytes(source, off, len, NO_OPTIONS); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation. <p/> Valid options: + * + * <pre> + * GZIP: gzip-compresses object before encoding it. + * DONT_BREAK_LINES: don't break lines at 76 characters + * <i>Note: Technically, this makes your encoding non-compliant.</i> + * </pre> + * + * <p/> Example: <code>encodeBytes( myData, Base64.GZIP )</code> or <p/> + * Example: + * <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code> + * + * @param source + * The data to convert + * @param off + * Offset in array where conversion should begin + * @param len + * Length of data to convert + * @param options + * Specified options + * @see Base64#GZIP + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int off, int len, + int options) { + // Isolate options + int dontBreakLines = (options & DONT_BREAK_LINES); + int gzip = (options & GZIP); + + // Compress? + if (gzip == GZIP) { + java.io.ByteArrayOutputStream baos = null; + java.util.zip.GZIPOutputStream gzos = null; + Base64.OutputStream b64os = null; + + try { + // GZip -> Base64 -> ByteArray + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | dontBreakLines); + gzos = new java.util.zip.GZIPOutputStream(b64os); + + gzos.write(source, off, len); + gzos.close(); + } // end try + catch (java.io.IOException e) { + e.printStackTrace(); + return null; + } // end catch + finally { + try { + gzos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + // Return value according to relevant encoding. + try { + return new String(baos.toByteArray(), PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(baos.toByteArray()); + } // end catch + } // end if: compress + + // Else, don't compress. Better not to use streams at all then. + else { + // Convert option to boolean in way that code likes it. + boolean breakLines = dontBreakLines == 0; + + int len43 = len * 4 / 3; + byte[] outBuff = new byte[(len43) // Main 4:3 + + ((len % 3) > 0 ? 4 : 0) // Account for padding + + (breakLines ? (len43 / MAX_LINE_LENGTH) : 0)]; // New + // lines + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + encode3to4(source, d + off, 3, outBuff, e); + + lineLength += 4; + if (breakLines && lineLength == MAX_LINE_LENGTH) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // en dfor: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e); + e += 4; + } // end if: some padding needed + + // Return value according to relevant encoding. + try { + return new String(outBuff, 0, e, PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(outBuff, 0, e); + } // end catch + + } // end else: don't compress + + } // end encodeBytes + + /* ******** D E C O D I N G M E T H O D S ******** */ + + /** + * Decodes four bytes from array <var>source</var> and writes the resulting + * bytes (up to three of them) to <var>destination</var>. The source and + * destination arrays can be manipulated anywhere along their length by + * specifying <var>srcOffset</var> and <var>destOffset</var>. This method + * does not check to make sure your arrays are large enough to accomodate + * <var>srcOffset</var> + 4 for the <var>source</var> array or + * <var>destOffset</var> + 3 for the <var>destination</var> array. This + * method returns the actual number of bytes that were converted from the + * Base64 encoding. + * + * @param source + * the array to convert + * @param srcOffset + * the index where conversion begins + * @param destination + * the array to hold the conversion + * @param destOffset + * the index where output will be put + * @return the number of decoded bytes converted + * @since 1.3 + */ + private static int decode4to3(byte[] source, int srcOffset, + byte[] destination, int destOffset) { + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 + // ) + // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12); + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } + + // Example: DkL= + else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 + // ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6); + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } + + // Example: DkLE + else { + try { + // Two ways to do the same thing. Don't know which way I like + // best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) + // >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) + // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6) + | ((DECODABET[source[srcOffset + 3]] & 0xFF)); + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) (outBuff); + + return 3; + } catch (Exception e) { + System.out.println("" + source[srcOffset] + ": " + + (DECODABET[source[srcOffset]])); + System.out.println("" + source[srcOffset + 1] + ": " + + (DECODABET[source[srcOffset + 1]])); + System.out.println("" + source[srcOffset + 2] + ": " + + (DECODABET[source[srcOffset + 2]])); + System.out.println("" + source[srcOffset + 3] + ": " + + (DECODABET[source[srcOffset + 3]])); + return -1; + } // e nd catch + } + } // end decodeToBytes + + /** + * Very low-level access to decoding ASCII characters in the form of a byte + * array. Does not support automatically gunzipping or any other "fancy" + * features. + * + * @param source + * The Base64 encoded data + * @param off + * The offset of where to begin decoding + * @param len + * The length of characters to decode + * @return decoded data + * @since 1.3 + */ + public static byte[] decode(byte[] source, int off, int len) { + int len34 = len * 3 / 4; + byte[] outBuff = new byte[len34]; // Upper limit on size of output + int outBuffPosn = 0; + + byte[] b4 = new byte[4]; + int b4Posn = 0; + int i = 0; + byte sbiCrop = 0; + byte sbiDecode = 0; + for (i = off; i < off + len; i++) { + sbiCrop = (byte) (source[i] & 0x7f); // Only the low seven bits + sbiDecode = DECODABET[sbiCrop]; + + if (sbiDecode >= WHITE_SPACE_ENC) // White space, Equals sign or + // better + { + if (sbiDecode >= EQUALS_SIGN_ENC) { + b4[b4Posn++] = sbiCrop; + if (b4Posn > 3) { + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn); + b4Posn = 0; + + // If that was the equals sign, break out of 'for' loop + if (sbiCrop == EQUALS_SIGN) { + break; + } + } // end if: quartet built + + } // end if: equals sign or better + + } // end if: white space, equals sign or better + else { + System.err.println("Bad Base64 input character at " + i + ": " + + source[i] + "(decimal)"); + return null; + } // end else: + } // each input character + + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; + } // end decode + + /** + * Decodes data from Base64 notation, automatically detecting + * gzip-compressed data and decompressing it. + * + * @param s + * the string to decode + * @return the decoded data + * @since 1.4 + */ + public static byte[] decode(String s) { + byte[] bytes; + try { + bytes = s.getBytes(PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uee) { + bytes = s.getBytes(); + } // end catch + // </change> + + // Decode + bytes = decode(bytes, 0, bytes.length); + + // Check to see if it's gzip-compressed + // GZIP Magic Two-Byte Number: 0x8b1f (35615) + if (bytes != null && bytes.length >= 4) { + + int head = ((int) bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); + if (java.util.zip.GZIPInputStream.GZIP_MAGIC == head) { + java.io.ByteArrayInputStream bais = null; + java.util.zip.GZIPInputStream gzis = null; + java.io.ByteArrayOutputStream baos = null; + byte[] buffer = new byte[2048]; + int length = 0; + + try { + baos = new java.io.ByteArrayOutputStream(); + bais = new java.io.ByteArrayInputStream(bytes); + gzis = new java.util.zip.GZIPInputStream(bais); + + while ((length = gzis.read(buffer)) >= 0) { + baos.write(buffer, 0, length); + } // end while: reading input + + // No error? Get new bytes. + bytes = baos.toByteArray(); + + } // end try + catch (java.io.IOException e) { + // Just return originally-decoded bytes + } // end catch + finally { + try { + baos.close(); + } catch (Exception e) { + } + try { + gzis.close(); + } catch (Exception e) { + } + try { + bais.close(); + } catch (Exception e) { + } + } // end finally + + } // end if: gzipped + } // end if: bytes.length >= 2 + + return bytes; + } // end decode + + /** + * Attempts to decode Base64 data and deserialize a Java Object within. + * Returns <tt>null</tt> if there was an error. + * + * @param encodedObject + * The Base64 data to decode + * @return The decoded and deserialized object + * @since 1.5 + */ + public static Object decodeToObject(String encodedObject) { + // Decode and gunzip if necessary + byte[] objBytes = decode(encodedObject); + + java.io.ByteArrayInputStream bais = null; + java.io.ObjectInputStream ois = null; + Object obj = null; + + try { + bais = new java.io.ByteArrayInputStream(objBytes); + ois = new java.io.ObjectInputStream(bais); + + obj = ois.readObject(); + } // end try + catch (java.io.IOException e) { + e.printStackTrace(); + obj = null; + } // end catch + catch (java.lang.ClassNotFoundException e) { + e.printStackTrace(); + obj = null; + } // end catch + finally { + try { + bais.close(); + } catch (Exception e) { + } + try { + ois.close(); + } catch (Exception e) { + } + } // end finally + + return obj; + } // end decodeObject + + /** + * Convenience method for encoding data to a file. + * + * @param dataToEncode + * byte array of data to encode in base64 form + * @param filename + * Filename for saving encoded data + * @return <tt>true</tt> if successful, <tt>false</tt> otherwise + * @since 2.1 + */ + public static boolean encodeToFile(byte[] dataToEncode, String filename) { + boolean success = false; + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream( + new java.io.FileOutputStream(filename), Base64.ENCODE); + bos.write(dataToEncode); + success = true; + } // end try + catch (java.io.IOException e) { + + success = false; + } // end catch: IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + return success; + } // end encodeToFile + + /** + * Convenience method for decoding data to a file. + * + * @param dataToDecode + * Base64-encoded data as a string + * @param filename + * Filename for saving decoded data + * @return <tt>true</tt> if successful, <tt>false</tt> otherwise + * @since 2.1 + */ + public static boolean decodeToFile(String dataToDecode, String filename) { + boolean success = false; + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream( + new java.io.FileOutputStream(filename), Base64.DECODE); + bos.write(dataToDecode.getBytes(PREFERRED_ENCODING)); + success = true; + } // end try + catch (java.io.IOException e) { + success = false; + } // end catch: IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + return success; + } // end decodeToFile + + /** + * Convenience method for reading a base64-encoded file and decoding it. + * + * @param filename + * Filename for reading encoded data + * @return decoded byte array or null if unsuccessful + * @since 2.1 + */ + public static byte[] decodeFromFile(String filename) { + byte[] decodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = null; + int length = 0; + int numBytes = 0; + + // Check for size of file + if (file.length() > Integer.MAX_VALUE) { + System.err + .println("File is too big for this convenience method (" + + file.length() + " bytes)."); + return null; + } // end if: file too big for int index + buffer = new byte[(int) file.length()]; + + // Open a stream + bis = new Base64.InputStream(new java.io.BufferedInputStream( + new java.io.FileInputStream(file)), Base64.DECODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { + length += numBytes; + } + + // Save in a variable to return + decodedData = new byte[length]; + System.arraycopy(buffer, 0, decodedData, 0, length); + + } // end try + catch (java.io.IOException e) { + System.err.println("Error decoding from file " + filename); + } // end catch: IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return decodedData; + } // end decodeFromFile + + /** + * Convenience method for reading a binary file and base64-encoding it. + * + * @param filename + * Filename for reading binary data + * @return base64-encoded string or null if unsuccessful + * @since 2.1 + */ + public static String encodeFromFile(String filename) { + String encodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = new byte[(int) (file.length() * 1.4)]; + int length = 0; + int numBytes = 0; + + // Open a stream + bis = new Base64.InputStream(new java.io.BufferedInputStream( + new java.io.FileInputStream(file)), Base64.ENCODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { + length += numBytes; + } + + // Save in a variable to return + encodedData = new String(buffer, 0, length, + Base64.PREFERRED_ENCODING); + + } // end try + catch (java.io.IOException e) { + System.err.println("Error encoding from file " + filename); + } // end catch: IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return encodedData; + } // end encodeFromFile + + /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ + + /** + * A {@link Base64.InputStream} will read data from another + * <tt>java.io.InputStream</tt>, given in the constructor, and + * encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class InputStream extends java.io.FilterInputStream { + private boolean encode; // Encoding or decoding + + private int position; // Current position in the buffer + + private byte[] buffer; // Small buffer holding converted data + + private int bufferLength; // Length of buffer (3 or 4) + + private int numSigBytes; // Number of meaningful bytes in the buffer + + private int lineLength; + + private boolean breakLines; // Break lines at less than 80 characters + + /** + * Constructs a {@link Base64.InputStream} in DECODE mode. + * + * @param in + * the <tt>java.io.InputStream</tt> from which to read + * data. + * @since 1.3 + */ + public InputStream(java.io.InputStream in) { + this(in, DECODE); + } // end constructor + + /** + * Constructs a {@link Base64.InputStream} in either ENCODE or DECODE + * mode. <p/> Valid options: + * + * <pre> + * ENCODE or DECODE: Encode or Decode as data is read. + * DONT_BREAK_LINES: don't break lines at 76 characters + * (only meaningful when encoding) + * <i>Note: Technically, this makes your encoding non-compliant.</i> + * </pre> + * + * <p/> Example: + * <code>new Base64.InputStream( in, Base64.DECODE )</code> + * + * @param in + * the <tt>java.io.InputStream</tt> from which to read + * data. + * @param options + * Specified options + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DONT_BREAK_LINES + * @since 2.0 + */ + public InputStream(java.io.InputStream in, int options) { + super(in); + this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; + this.encode = (options & ENCODE) == ENCODE; + this.bufferLength = encode ? 4 : 3; + this.buffer = new byte[bufferLength]; + this.position = -1; + this.lineLength = 0; + } // end constructor + + /** + * Reads enough of the input stream to convert to/from Base64 and + * returns the next byte. + * + * @return next byte + * @since 1.3 + */ + public int read() throws java.io.IOException { + // Do we need to get data? + if (position < 0) { + if (encode) { + byte[] b3 = new byte[3]; + int numBinaryBytes = 0; + for (int i = 0; i < 3; i++) { + try { + int b = in.read(); + + // If end of stream, b is -1. + if (b >= 0) { + b3[i] = (byte) b; + numBinaryBytes++; + } // end if: not end of stream + + } // end try: read + catch (java.io.IOException e) { + // Only a problem if we got no data at all. + if (i == 0) { + throw e; + } + + } // end catch + } // end for: each needed input byte + + if (numBinaryBytes > 0) { + encode3to4(b3, 0, numBinaryBytes, buffer, 0); + position = 0; + numSigBytes = 4; + } // end if: got data + else { + return -1; + } // end else + } // end if: encoding + + // Else decoding + else { + byte[] b4 = new byte[4]; + int i = 0; + for (i = 0; i < 4; i++) { + // Read four "meaningful" bytes: + int b = 0; + do { + b = in.read(); + } while (b >= 0 + && DECODABET[b & 0x7f] <= WHITE_SPACE_ENC); + + if (b < 0) { + break; // Reads a -1 if end of stream + } + + b4[i] = (byte) b; + } // end for: each needed input byte + + if (i == 4) { + numSigBytes = decode4to3(b4, 0, buffer, 0); + position = 0; + } // end if: got four characters + else if (i == 0) { + return -1; + } // end else if: also padded correctly + else { + // Must have broken out from above. + throw new java.io.IOException( + "Improperly padded Base64 input."); + } // end + + } // end else: decode + } // end else: get data + + // Got data? + if (position >= 0) { + // End of relevant data? + if (/* !encode && */position >= numSigBytes) { + return -1; + } + + if (encode && breakLines && lineLength >= MAX_LINE_LENGTH) { + lineLength = 0; + return '\n'; + } // end if + else { + lineLength++; // This isn't important when decoding + // but throwing an extra "if" seems + // just as wasteful. + + int b = buffer[position++]; + + if (position >= bufferLength) { + position = -1; + } + + return b & 0xFF; // This is how you "cast" a byte that's + // intended to be unsigned. + } // end else + } // end if: position >= 0 + + // Else error + else { + // When JDK1.4 is more accepted, use an assertion here. + throw new java.io.IOException( + "Error in Base64 code reading stream."); + } // end else + } // end read + + /** + * Calls {@link #read()} repeatedly until the end of stream is reached + * or <var>len</var> bytes are read. Returns number of bytes read into + * array or -1 if end of stream is encountered. + * + * @param dest + * array to hold values + * @param off + * offset for array + * @param len + * max number of bytes to read into array + * @return bytes read into array or -1 if end of stream is encountered. + * @since 1.3 + */ + public int read(byte[] dest, int off, int len) + throws java.io.IOException { + int i; + int b; + for (i = 0; i < len; i++) { + b = read(); + + // if( b < 0 && i == 0 ) + // return -1; + + if (b >= 0) { + dest[off + i] = (byte) b; + } else if (i == 0) { + return -1; + } else { + break; // Out of 'for' loop + } + } // end for: each byte read + return i; + } // end read + + } // end inner class InputStream + + /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ + + /** + * A {@link Base64.OutputStream} will write data to another + * <tt>java.io.OutputStream</tt>, given in the constructor, and + * encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class OutputStream extends java.io.FilterOutputStream { + private boolean encode; + + private int position; + + private byte[] buffer; + + private int bufferLength; + + private int lineLength; + + private boolean breakLines; + + private byte[] b4; // Scratch used in a few places + + private boolean suspendEncoding; + + /** + * Constructs a {@link Base64.OutputStream} in ENCODE mode. + * + * @param out + * the <tt>java.io.OutputStream</tt> to which data will be + * written. + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out) { + this(out, ENCODE); + } // end constructor + + /** + * Constructs a {@link Base64.OutputStream} in either ENCODE or DECODE + * mode. <p/> Valid options: + * + * <pre> + * ENCODE or DECODE: Encode or Decode as data is read. + * DONT_BREAK_LINES: don't break lines at 76 characters + * (only meaningful when encoding) + * <i>Note: Technically, this makes your encoding non-compliant.</i> + * </pre> + * + * <p/> Example: + * <code>new Base64.OutputStream( out, Base64.ENCODE )</code> + * + * @param out + * the <tt>java.io.OutputStream</tt> to which data will be + * written. + * @param options + * Specified options. + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DONT_BREAK_LINES + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out, int options) { + super(out); + this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES; + this.encode = (options & ENCODE) == ENCODE; + this.bufferLength = encode ? 3 : 4; + this.buffer = new byte[bufferLength]; + this.position = 0; + this.lineLength = 0; + this.suspendEncoding = false; + this.b4 = new byte[4]; + } // end constructor + + /** + * Writes the byte to the output stream after converting to/from Base64 + * notation. When encoding, bytes are buffered three at a time before + * the output stream actually gets a write() call. When decoding, bytes + * are buffered four at a time. + * + * @param theByte + * the byte to write + * @since 1.3 + */ + public void write(int theByte) throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + super.out.write(theByte); + return; + } // end if: supsended + + // Encode? + if (encode) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) // Enough to encode. + { + out.write(encode3to4(b4, buffer, bufferLength)); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) { + out.write(NEW_LINE); + lineLength = 0; + } // end if: end of line + + position = 0; + } // end if: enough to output + } // end if: encoding + + // Else, Decoding + else { + // Meaningful Base64 character? + if (DECODABET[theByte & 0x7f] > WHITE_SPACE_ENC) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) // Enough to output. + { + int len = Base64.decode4to3(buffer, 0, b4, 0); + out.write(b4, 0, len); + // out.write( Base64.decode4to3( buffer ) ); + position = 0; + } // end if: enough to output + } // end if: meaningful base64 character + else if (DECODABET[theByte & 0x7f] != WHITE_SPACE_ENC) { + throw new java.io.IOException( + "Invalid character in Base64 data."); + } // end else: not white space either + } // end else: decoding + } // end write + + /** + * Calls {@link #write(int)} repeatedly until <var>len</var> bytes are + * written. + * + * @param theBytes + * array from which to read bytes + * @param off + * offset for array + * @param len + * max number of bytes to read into array + * @since 1.3 + */ + public void write(byte[] theBytes, int off, int len) + throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + super.out.write(theBytes, off, len); + return; + } // end if: supsended + + for (int i = 0; i < len; i++) { + write(theBytes[off + i]); + } // end for: each byte written + + } // end write + + /** + * Method added by PHIL. [Thanks, PHIL. -Rob] This pads the buffer + * without closing the stream. + */ + public void flushBase64() throws java.io.IOException { + if (position > 0) { + if (encode) { + out.write(encode3to4(b4, buffer, position)); + position = 0; + } // end if: encoding + else { + throw new java.io.IOException( + "Base64 input not properly padded."); + } // end else: decoding + } // end if: buffer partially full + + } // end flush + + /** + * Flushes and closes (I think, in the superclass) the stream. + * + * @since 1.3 + */ + public void close() throws java.io.IOException { + // 1. Ensure that pending characters are written + flushBase64(); + + // 2. Actually close the stream + // Base class both flushes and closes. + super.close(); + + buffer = null; + out = null; + } // end close + + /** + * Suspends encoding of the stream. May be helpful if you need to embed + * a piece of base640-encoded data in a stream. + * + * @since 1.5.1 + */ + public void suspendEncoding() throws java.io.IOException { + flushBase64(); + this.suspendEncoding = true; + } // end suspendEncoding + + /** + * Resumes encoding of the stream. May be helpful if you need to embed a + * piece of base640-encoded data in a stream. + * + * @since 1.5.1 + */ + public void resumeEncoding() { + this.suspendEncoding = false; + } // end resumeEncoding + + } // end inner class OutputStream + +} // end class Base64 + diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java new file mode 100644 index 000000000..11a84d471 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java @@ -0,0 +1,130 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.OrFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * The fault tolerant negotiator takes two stream negotiators, the primary and the secondary negotiator. + * If the primary negotiator fails during the stream negotiaton process, the second negotiator is used. + */ +public class FaultTolerantNegotiator extends StreamNegotiator { + + private StreamNegotiator primaryNegotiator; + private StreamNegotiator secondaryNegotiator; + private XMPPConnection connection; + private PacketFilter primaryFilter; + private PacketFilter secondaryFilter; + + public FaultTolerantNegotiator(XMPPConnection connection, StreamNegotiator primary, StreamNegotiator secondary) { + this.primaryNegotiator = primary; + this.secondaryNegotiator = secondary; + this.connection = connection; + } + + public PacketFilter getInitiationPacketFilter(String from, String streamID) { + if (primaryFilter == null || secondaryFilter == null) { + primaryFilter = primaryNegotiator.getInitiationPacketFilter(from, streamID); + secondaryFilter = secondaryNegotiator.getInitiationPacketFilter(from, streamID); + } + return new OrFilter(primaryFilter, secondaryFilter); + } + + InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException { + throw new UnsupportedOperationException("Negotiation only handled by create incoming stream method."); + } + + final Packet initiateIncomingStream(XMPPConnection connection, StreamInitiation initiation) throws XMPPException { + throw new UnsupportedOperationException("Initiation handled by createIncomingStream method"); + } + + public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException { + PacketFilter filter = getInitiationPacketFilter(initiation.getFrom(), initiation.getSessionID()); + PacketCollector collector = connection.createPacketCollector(filter); + + StreamInitiation response = super.createInitiationAccept(initiation, getNamespaces()); + connection.sendPacket(response); + + InputStream stream; + try { + Packet streamInitiation = collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + if (streamInitiation == null) { + throw new XMPPException("No response from remote client"); + } + StreamNegotiator negotiator = determineNegotiator(streamInitiation); + stream = negotiator.negotiateIncomingStream(streamInitiation); + } + catch (XMPPException ex) { + ex.printStackTrace(); + Packet streamInitiation = collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (streamInitiation == null) { + throw new XMPPException("No response from remote client"); + } + StreamNegotiator negotiator = determineNegotiator(streamInitiation); + stream = negotiator.negotiateIncomingStream(streamInitiation); + } finally { + collector.cancel(); + } + + return stream; + } + + private StreamNegotiator determineNegotiator(Packet streamInitiation) { + return primaryFilter.accept(streamInitiation) ? primaryNegotiator : secondaryNegotiator; + } + + public OutputStream createOutgoingStream(String streamID, String initiator, String target) throws XMPPException { + OutputStream stream; + try { + stream = primaryNegotiator.createOutgoingStream(streamID, initiator, target); + } + catch (XMPPException ex) { + stream = secondaryNegotiator.createOutgoingStream(streamID, initiator, target); + } + + return stream; + } + + public String[] getNamespaces() { + String [] primary = primaryNegotiator.getNamespaces(); + String [] secondary = secondaryNegotiator.getNamespaces(); + + String [] namespaces = new String[primary.length + secondary.length]; + System.arraycopy(primary, 0, namespaces, 0, primary.length); + System.arraycopy(secondary, 0, namespaces, primary.length, secondary.length); + + return namespaces; + } + + public void cleanup() { + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransfer.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransfer.java new file mode 100644 index 000000000..c58d44736 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransfer.java @@ -0,0 +1,353 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.filetransfer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.jivesoftware.smack.XMPPException; + +/** + * Contains the generic file information and progress related to a particular + * file transfer. + * + * @author Alexander Wenckus + * + */ +public abstract class FileTransfer { + + private String fileName; + + private String filePath; + + private long fileSize; + + private String peer; + + private org.jivesoftware.smackx.filetransfer.FileTransfer.Status status; + + protected FileTransferNegotiator negotiator; + + protected String streamID; + + protected long amountWritten = -1; + + private Error error; + + private Exception exception; + + protected FileTransfer(String peer, String streamID, + FileTransferNegotiator negotiator) { + this.peer = peer; + this.streamID = streamID; + this.negotiator = negotiator; + } + + protected void setFileInfo(String fileName, long fileSize) { + this.fileName = fileName; + this.fileSize = fileSize; + } + + protected void setFileInfo(String path, String fileName, long fileSize) { + this.filePath = path; + this.fileName = fileName; + this.fileSize = fileSize; + } + + /** + * Returns the size of the file being transfered. + * + * @return Returns the size of the file being transfered. + */ + public long getFileSize() { + return fileSize; + } + + /** + * Returns the name of the file being transfered. + * + * @return Returns the name of the file being transfered. + */ + public String getFileName() { + return fileName; + } + + /** + * Returns the local path of the file. + * + * @return Returns the local path of the file. + */ + public String getFilePath() { + return filePath; + } + + /** + * Returns the JID of the peer for this file transfer. + * + * @return Returns the JID of the peer for this file transfer. + */ + public String getPeer() { + return peer; + } + + /** + * Returns the progress of the file transfer as a number between 0 and 1. + * + * @return Returns the progress of the file transfer as a number between 0 + * and 1. + */ + public double getProgress() { + if(amountWritten == 0) { + return 0; + } + return amountWritten / fileSize; + } + + /** + * Returns true if the transfer has been cancled, if it has stopped because + * of a an error, or the transfer completed succesfully. + * + * @return Returns true if the transfer has been cancled, if it has stopped + * because of a an error, or the transfer completed succesfully. + */ + public boolean isDone() { + return status == Status.CANCLED || status == Status.ERROR + || status == Status.COMPLETE; + } + + /** + * Retuns the current status of the file transfer. + * + * @return Retuns the current status of the file transfer. + */ + public Status getStatus() { + return status; + } + + protected void setError(Error type) { + this.error = type; + } + + /** + * When {@link #getStatus()} returns that there was an {@link Status#ERROR} + * during the transfer, the type of error can be retrieved through this + * method. + * + * @return Returns the type of error that occured if one has occured. + */ + public Error getError() { + return error; + } + + /** + * If an exception occurs asynchronously it will be stored for later + * retrival. If there is an error there maybe an exception set. + * + * @return The exception that occured or null if there was no exception. + * @see #getError() + */ + public Exception getException() { + return exception; + } + + /** + * Cancels the file transfer. + */ + public abstract void cancel(); + + protected void setException(Exception exception) { + this.exception = exception; + } + + protected final void setStatus(Status status) { + this.status = status; + } + + protected void writeToStream(final InputStream in, final OutputStream out) + throws XMPPException { + final byte[] b = new byte[1000]; + int count = 0; + amountWritten = 0; + try { + count = in.read(b); + } catch (IOException e) { + throw new XMPPException("error reading from input stream", e); + } + while (count != -1 && !getStatus().equals(Status.CANCLED)) { + if (getStatus().equals(Status.CANCLED)) { + return; + } + + // write to the output stream + try { + out.write(b, 0, count); + } catch (IOException e) { + throw new XMPPException("error writing to output stream", e); + } + + amountWritten += count; + + // read more bytes from the input stream + try { + count = in.read(b); + } catch (IOException e) { + throw new XMPPException("error reading from input stream", e); + } + } + + // the connection was likely terminated abrubtly if these are not + // equal + if (!getStatus().equals(Status.CANCLED) && getError() == Error.NONE + && amountWritten != fileSize) { + this.error = Error.CONNECTION; + } + } + + /** + * A class to represent the current status of the file transfer. + * + * @author Alexander Wenckus + * + */ + public static class Status { + /** + * An error occured during the transfer. + * + * @see FileTransfer#getError() + */ + public static final Status ERROR = new Status(); + + /** + * The file transfer is being negotiated with the peer. The party + * recieving the file has the option to accept or refuse a file transfer + * request. If they accept, then the process of stream negotiation will + * begin. If they refuse the file will not be transfered. + * + * @see #NEGOTIATING_STREAM + */ + public static final Status NEGOTIATING_TRANSFER = new Status(); + + /** + * The peer has refused the file transfer request halting the file + * transfer negotiation process. + */ + public static final Status REFUSED = new Status(); + + /** + * The stream to transfer the file is being negotiated over the chosen + * stream type. After the stream negotiating process is complete the + * status becomes negotiated. + * + * @see #NEGOTIATED + */ + public static final Status NEGOTIATING_STREAM = new Status(); + + /** + * After the stream negotitation has completed the intermediate state + * between the time when the negotiation is finished and the actual + * transfer begins. + */ + public static final Status NEGOTIATED = new Status(); + + /** + * The transfer is in progress. + * + * @see FileTransfer#getProgress() + */ + public static final Status IN_PROGRESS = new Status(); + + /** + * The transfer has completed successfully. + */ + public static final Status COMPLETE = new Status(); + + /** + * The file transfer was canceled + */ + public static final Status CANCLED = new Status(); + } + + /** + * Return the length of bytes written out to the stream. + * @return the amount in bytes written out. + */ + public long getAmountWritten(){ + return amountWritten; + } + + public static class Error { + /** + * No error + */ + public static final Error NONE = new Error("No error"); + + /** + * The peer did not find any of the provided stream mechanisms + * acceptable. + */ + public static final Error NOT_ACCEPTABLE = new Error( + "The peer did not find any of the provided stream mechanisms acceptable."); + + /** + * The provided file to transfer does not exist or could not be read. + */ + public static final Error BAD_FILE = new Error( + "The provided file to transfer does not exist or could not be read."); + + /** + * The remote user did not respond or the connection timed out. + */ + public static final Error NO_RESPONSE = new Error( + "The remote user did not respond or the connection timed out."); + + /** + * An error occured over the socket connected to send the file. + */ + public static final Error CONNECTION = new Error( + "An error occured over the socket connected to send the file."); + + /** + * An error occured while sending or recieving the file + */ + protected static final Error STREAM = new Error( + "An error occured while sending or recieving the file"); + + private final String msg; + + private Error(String msg) { + this.msg = msg; + } + + /** + * Returns a String representation of this error. + * + * @return Returns a String representation of this error. + */ + public String getMessage() { + return msg; + } + + public String toString() { + return msg; + } + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferListener.java new file mode 100644 index 000000000..904623cb9 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferListener.java @@ -0,0 +1,36 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.filetransfer; + +/** + * File transfers can cause several events to be raised. These events can be + * monitored through this interface. + * + * @author Alexander Wenckus + */ +public interface FileTransferListener { + /** + * A request to send a file has been recieved from another user. + * + * @param request + * The request from the other user. + */ + public void fileTransferRequest(final FileTransferRequest request); +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferManager.java new file mode 100644 index 000000000..de4f7f10a --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferManager.java @@ -0,0 +1,178 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.filetransfer; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.IQTypeFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.packet.StreamInitiation; + +/** + * The file transfer manager class handles the sending and recieving of files. + * To send a file invoke the {@link #createOutgoingFileTransfer(String)} method. + * <p> + * And to recieve a file add a file transfer listener to the manager. The + * listener will notify you when there is a new file transfer request. To create + * the {@link IncomingFileTransfer} object accept the transfer, or, if the + * transfer is not desirable reject it. + * + * @author Alexander Wenckus + * + */ +public class FileTransferManager { + + private final FileTransferNegotiator fileTransferNegotiator; + + private List listeners; + + private XMPPConnection connection; + + /** + * Creates a file transfer manager to initiate and receive file transfers. + * + * @param connection + * The XMPPConnection that the file transfers will use. + */ + public FileTransferManager(XMPPConnection connection) { + this.connection = connection; + this.fileTransferNegotiator = FileTransferNegotiator + .getInstanceFor(connection); + } + + /** + * Add a file transfer listener to listen to incoming file transfer + * requests. + * + * @param li + * The listener + * @see #removeFileTransferListener(FileTransferListener) + * @see FileTransferListener + */ + public void addFileTransferListener(final FileTransferListener li) { + if (listeners == null) { + initListeners(); + } + synchronized (this.listeners) { + listeners.add(li); + } + } + + private void initListeners() { + listeners = new ArrayList(); + + connection.addPacketListener(new PacketListener() { + public void processPacket(Packet packet) { + fireNewRequest((StreamInitiation) packet); + } + }, new AndFilter(new PacketTypeFilter(StreamInitiation.class), + new IQTypeFilter(IQ.Type.SET))); + } + + protected void fireNewRequest(StreamInitiation initiation) { + FileTransferListener[] listeners = null; + synchronized (this.listeners) { + listeners = new FileTransferListener[this.listeners.size()]; + this.listeners.toArray(listeners); + } + FileTransferRequest request = new FileTransferRequest(this, initiation); + for (int i = 0; i < listeners.length; i++) { + listeners[i].fileTransferRequest(request); + } + } + + /** + * Removes a file transfer listener. + * + * @param li + * The file transfer listener to be removed + * @see FileTransferListener + */ + public void removeFileTransferListener(final FileTransferListener li) { + if (listeners == null) { + return; + } + synchronized (this.listeners) { + listeners.remove(li); + } + } + + /** + * Creates an OutgoingFileTransfer to send a file to another user. + * + * @param userID + * The fully qualified jabber ID with resource of the user to + * send the file to. + * @return The send file object on which the negotiated transfer can be run. + */ + public OutgoingFileTransfer createOutgoingFileTransfer(String userID) { + if (userID == null || StringUtils.parseName(userID).length() <= 0 + || StringUtils.parseServer(userID).length() <= 0 + || StringUtils.parseResource(userID).length() <= 0) { + throw new IllegalArgumentException( + "The provided user id was not fully qualified"); + } + + return new OutgoingFileTransfer(connection.getUser(), userID, + fileTransferNegotiator.getNextStreamID(), + fileTransferNegotiator); + } + + /** + * When the file transfer request is acceptable, this method should be + * invoked. It will create an IncomingFileTransfer which allows the + * transmission of the file to procede. + * + * @param request + * The remote request that is being accepted. + * @return The IncomingFileTransfer which manages the download of the file + * from the transfer initiator. + */ + protected IncomingFileTransfer createIncomingFileTransfer( + FileTransferRequest request) { + if (request == null) { + throw new NullPointerException("RecieveRequest cannot be null"); + } + + IncomingFileTransfer transfer = new IncomingFileTransfer(request, + fileTransferNegotiator); + transfer.setFileInfo(request.getFileName(), request.getFileSize()); + + return transfer; + } + + protected void rejectIncomingFileTransfer(FileTransferRequest request) { + StreamInitiation initiation = request.getStreamInitiation(); + + IQ rejection = FileTransferNegotiator.createIQ( + initiation.getPacketID(), initiation.getFrom(), initiation + .getTo(), IQ.Type.ERROR); + rejection.setError(new XMPPError(403)); + connection.sendPacket(rejection); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java new file mode 100644 index 000000000..1123b7fe7 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java @@ -0,0 +1,438 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DataForm; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.net.URLConnection; +import java.util.*; + +/** + * Manages the negotiation of file transfers according to JEP-0096. If a file is + * being sent the remote user chooses the type of stream under which the file + * will be sent. + * + * @author Alexander Wenckus + * @see <a href=http://www.jabber.org/jeps/jep-0096.html>JEP-0096: File Transfer</a> + */ +public class FileTransferNegotiator { + + // Static + + /** + * The XMPP namespace of the SOCKS5 bytestream + */ + public static final String BYTE_STREAM = "http://jabber.org/protocol/bytestreams"; + + /** + * The XMPP namespace of the In-Band bytestream + */ + public static final String INBAND_BYTE_STREAM = "http://jabber.org/protocol/ibb"; + + private static final String[] NAMESPACE = { + "http://jabber.org/protocol/si/profile/file-transfer", + "http://jabber.org/protocol/si", BYTE_STREAM, INBAND_BYTE_STREAM}; + + private static final String[] PROTOCOLS = {BYTE_STREAM, INBAND_BYTE_STREAM}; + + private static final Map transferObject = new HashMap(); + + private static final String STREAM_INIT_PREFIX = "jsi_"; + + protected static final String STREAM_DATA_FIELD_NAME = "stream-method"; + + private static final Random randomGenerator = new Random(); + + public static boolean IBB_ONLY = false; + + /** + * Returns the file transfer negotiator related to a particular connection. + * When this class is requested on a particular connection the file transfer + * service is automatically enabled. + * + * @param connection The connection for which the transfer manager is desired + * @return The IMFileTransferManager + */ + public static FileTransferNegotiator getInstanceFor( + final XMPPConnection connection) { + if (connection == null) { + throw new IllegalArgumentException("Connection cannot be null"); + } + if (!connection.isConnected()) { + return null; + } + + if (transferObject.containsKey(connection)) { + return (FileTransferNegotiator) transferObject.get(connection); + } + else { + FileTransferNegotiator transfer = new FileTransferNegotiator( + connection); + setServiceEnabled(connection, true); + transferObject.put(connection, transfer); + return transfer; + } + } + + /** + * Enable the Jabber services related to file transfer on the particular + * connection. + * + * @param connection The connection on which to enable or disable the services. + * @param isEnabled True to enable, false to disable. + */ + public static void setServiceEnabled(final XMPPConnection connection, + final boolean isEnabled) { + ServiceDiscoveryManager manager = ServiceDiscoveryManager + .getInstanceFor(connection); + for (int i = 0; i < NAMESPACE.length; i++) { + if (isEnabled) { + manager.addFeature(NAMESPACE[i]); + } + else { + manager.removeFeature(NAMESPACE[i]); + } + } + } + + /** + * Checks to see if all file transfer related services are enabled on the + * connection. + * + * @param connection The connection to check + * @return True if all related services are enabled, false if they are not. + */ + public static boolean isServiceEnabled(final XMPPConnection connection) { + for (int i = 0; i < NAMESPACE.length; i++) { + if (!ServiceDiscoveryManager.getInstanceFor(connection) + .includesFeature(NAMESPACE[i])) + return false; + } + return true; + } + + /** + * A convience method to create an IQ packet. + * + * @param ID The packet ID of the + * @param to To whom the packet is addressed. + * @param from From whom the packet is sent. + * @param type The iq type of the packet. + * @return The created IQ packet. + */ + protected static IQ createIQ(final String ID, final String to, + final String from, final IQ.Type type) { + IQ iqPacket = new IQ() { + public String getChildElementXML() { + return null; + } + }; + iqPacket.setPacketID(ID); + iqPacket.setTo(to); + iqPacket.setFrom(from); + iqPacket.setType(type); + + return iqPacket; + } + + /** + * Returns a collection of the supported transfer protocols. + * + * @return Returns a collection of the supported transfer protocols. + */ + public static Collection getSupportedProtocols() { + return Collections.unmodifiableList(Arrays.asList(PROTOCOLS)); + } + + // non-static + + private final XMPPConnection connection; + + private final StreamNegotiator byteStreamTransferManager; + + private final StreamNegotiator inbandTransferManager; + + private FileTransferNegotiator(final XMPPConnection connection) { + configureConnection(connection); + + this.connection = connection; + byteStreamTransferManager = new Socks5TransferNegotiator(connection); + inbandTransferManager = new IBBTransferNegotiator(connection); + } + + private void configureConnection(final XMPPConnection connection) { + connection.addConnectionListener(new ConnectionListener() { + public void connectionClosed() { + cleanup(connection); + } + + public void connectionClosedOnError(Exception e) { + cleanup(connection); + } + }); + } + + private void cleanup(final XMPPConnection connection) { + transferObject.remove(connection); + + byteStreamTransferManager.cleanup(); + inbandTransferManager.cleanup(); + } + + /** + * Selects an appropriate stream negotiator after examining the incoming file transfer request. + * + * @param request The related file transfer request. + * @return The file transfer object that handles the transfer + * @throws XMPPException If there are either no stream methods contained in the packet, or + * there is not an appropriate stream method. + */ + public StreamNegotiator selectStreamNegotiator( + FileTransferRequest request) throws XMPPException { + StreamInitiation si = request.getStreamInitiation(); + FormField streamMethodField = getStreamMethodField(si + .getFeatureNegotiationForm()); + + if (streamMethodField == null) { + XMPPError error = new XMPPError(400); + IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), + IQ.Type.ERROR); + iqPacket.setError(error); + connection.sendPacket(iqPacket); + throw new XMPPException("No stream methods contained in packet.", error); + } + + // select the appropriate protocol + + StreamNegotiator selectedStreamNegotiator; + try { + selectedStreamNegotiator = getNegotiator(streamMethodField); + } + catch (XMPPException e) { + IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), + IQ.Type.ERROR); + iqPacket.setError(e.getXMPPError()); + connection.sendPacket(iqPacket); + throw e; + } + + // return the appropriate negotiator + + return selectedStreamNegotiator; + } + + private FormField getStreamMethodField(DataForm form) { + FormField field = null; + for (Iterator it = form.getFields(); it.hasNext();) { + field = (FormField) it.next(); + if (field.getVariable().equals(STREAM_DATA_FIELD_NAME)) { + break; + } + field = null; + } + return field; + } + + private StreamNegotiator getNegotiator(final FormField field) + throws XMPPException { + String variable; + boolean isByteStream = false; + boolean isIBB = false; + for (Iterator it = field.getOptions(); it.hasNext();) { + variable = ((FormField.Option) it.next()).getValue(); + if (variable.equals(BYTE_STREAM) && !IBB_ONLY) { + isByteStream = true; + } + else if (variable.equals(INBAND_BYTE_STREAM)) { + isIBB = true; + } + } + + if (!isByteStream && !isIBB) { + XMPPError error = new XMPPError(400); + throw new XMPPException("No acceptable transfer mechanism", error); + } + + if (isByteStream && isIBB && field.getType().equals(FormField.TYPE_LIST_MULTI)) { + return new FaultTolerantNegotiator(connection, byteStreamTransferManager, inbandTransferManager); + } + else if (isByteStream) { + return byteStreamTransferManager; + } + else { + return inbandTransferManager; + } + } + + /** + * Reject a stream initiation request from a remote user. + * + * @param si The Stream Initiation request to reject. + */ + public void rejectStream(final StreamInitiation si) { + XMPPError error = new XMPPError(403, "Offer Declined"); + IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(), + IQ.Type.ERROR); + iqPacket.setError(error); + connection.sendPacket(iqPacket); + } + + /** + * Returns a new, unique, stream ID to identify a file transfer. + * + * @return Returns a new, unique, stream ID to identify a file transfer. + */ + public String getNextStreamID() { + StringBuffer buffer = new StringBuffer(); + buffer.append(STREAM_INIT_PREFIX); + buffer.append(Math.abs(randomGenerator.nextLong())); + + return buffer.toString(); + } + + /** + * Send a request to another user to send them a file. The other user has + * the option of, accepting, rejecting, or not responding to a received file + * transfer request. + * <p/> + * If they accept, the packet will contain the other user's choosen stream + * type to send the file across. The two choices this implementation + * provides to the other user for file transfer are <a + * href="http://www.jabber.org/jeps/jep-0065.html">SOCKS5 Bytestreams</a>, + * which is the prefered method of transfer, and <a + * href="http://www.jabber.org/jeps/jep-0047.html">In-Band Bytestreams</a>, + * which is the fallback mechanism. + * <p/> + * The other user may choose to decline the file request if they do not + * desire the file, their client does not support JEP-0096, or if there are + * no acceptable means to transfer the file. + * <p/> + * Finally, if the other user does not respond this method will return null + * after the specified timeout. + * + * @param userID The userID of the user to whom the file will be sent. + * @param streamID The unique identifier for this file transfer. + * @param fileName The name of this file. Preferably it should include an + * extension as it is used to determine what type of file it is. + * @param size The size, in bytes, of the file. + * @param desc A description of the file. + * @param responseTimeout The amount of time, in milliseconds, to wait for the remote + * user to respond. If they do not respond in time, this + * @return Returns the stream negotiator selected by the peer. + * @throws XMPPException Thrown if there is an error negotiating the file transfer. + */ + public StreamNegotiator negotiateOutgoingTransfer(final String userID, + final String streamID, final String fileName, final long size, + final String desc, int responseTimeout) throws XMPPException { + StreamInitiation si = new StreamInitiation(); + si.setSesssionID(streamID); + si.setMimeType(URLConnection.guessContentTypeFromName(fileName)); + + StreamInitiation.File siFile = new StreamInitiation.File(fileName, size); + siFile.setDesc(desc); + si.setFile(siFile); + + si.setFeatureNegotiationForm(createDefaultInitiationForm()); + + si.setFrom(connection.getUser()); + si.setTo(userID); + si.setType(IQ.Type.SET); + + PacketCollector collector = connection + .createPacketCollector(new PacketIDFilter(si.getPacketID())); + connection.sendPacket(si); + Packet siResponse = collector.nextResult(responseTimeout); + collector.cancel(); + + if (siResponse instanceof IQ) { + IQ iqResponse = (IQ) siResponse; + if (iqResponse.getType().equals(IQ.Type.RESULT)) { + StreamInitiation response = (StreamInitiation) siResponse; + return getOutgoingNegotiator(getStreamMethodField(response + .getFeatureNegotiationForm())); + + } + else if (iqResponse.getType().equals(IQ.Type.ERROR)) { + throw new XMPPException(iqResponse.getError()); + } + else { + throw new XMPPException("File transfer response unreadable"); + } + } + else { + return null; + } + } + + private StreamNegotiator getOutgoingNegotiator(final FormField field) + throws XMPPException { + String variable; + boolean isByteStream = false; + boolean isIBB = false; + for (Iterator it = field.getValues(); it.hasNext();) { + variable = (it.next().toString()); + if (variable.equals(BYTE_STREAM) && !IBB_ONLY) { + isByteStream = true; + } + else if (variable.equals(INBAND_BYTE_STREAM)) { + isIBB = true; + } + } + + if (!isByteStream && !isIBB) { + XMPPError error = new XMPPError(400); + throw new XMPPException("No acceptable transfer mechanism", error); + } + + if (isByteStream && isIBB) { + return new FaultTolerantNegotiator(connection, byteStreamTransferManager, inbandTransferManager); + } + else if (isByteStream) { + return byteStreamTransferManager; + } + else { + return inbandTransferManager; + } + } + + private DataForm createDefaultInitiationForm() { + DataForm form = new DataForm(Form.TYPE_FORM); + FormField field = new FormField(STREAM_DATA_FIELD_NAME); + field.setType(FormField.TYPE_LIST_MULTI); + if (!IBB_ONLY) { + field.addOption(new FormField.Option(BYTE_STREAM)); + } + field.addOption(new FormField.Option(INBAND_BYTE_STREAM)); + form.addField(field); + return form; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java new file mode 100644 index 000000000..69a073f67 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java @@ -0,0 +1,138 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smackx.packet.StreamInitiation; + +/** + * A request to send a file recieved from another user. + * + * @author Alexander Wenckus + * + */ +public class FileTransferRequest { + private final StreamInitiation streamInitiation; + + private final FileTransferManager manager; + + /** + * A recieve request is constructed from the Stream Initiation request + * received from the initator. + * + * @param manager + * The manager handling this file transfer + * + * @param si + * The Stream initiaton recieved from the initiator. + */ + public FileTransferRequest(FileTransferManager manager, StreamInitiation si) { + this.streamInitiation = si; + this.manager = manager; + } + + /** + * Returns the name of the file. + * + * @return Returns the name of the file. + */ + public String getFileName() { + return streamInitiation.getFile().getName(); + } + + /** + * Returns the size in bytes of the file. + * + * @return Returns the size in bytes of the file. + */ + public long getFileSize() { + return streamInitiation.getFile().getSize(); + } + + /** + * Returns the description of the file provided by the requestor. + * + * @return Returns the description of the file provided by the requestor. + */ + public String getDescription() { + return streamInitiation.getFile().getDesc(); + } + + /** + * Returns the mime-type of the file. + * + * @return Returns the mime-type of the file. + */ + public String getMimeType() { + return streamInitiation.getMimeType(); + } + + /** + * Returns the fully-qualified jabber ID of the user that requested this + * file transfer. + * + * @return Returns the fully-qualified jabber ID of the user that requested + * this file transfer. + */ + public String getRequestor() { + return streamInitiation.getFrom(); + } + + /** + * Returns the stream ID that uniquely identifies this file transfer. + * + * @return Returns the stream ID that uniquely identifies this file + * transfer. + */ + public String getStreamID() { + return streamInitiation.getSessionID(); + } + + /** + * Returns the stream initiation packet that was sent by the requestor which + * contains the parameters of the file transfer being transfer and also the + * methods available to transfer the file. + * + * @return Returns the stream initiation packet that was sent by the + * requestor which contains the parameters of the file transfer + * being transfer and also the methods available to transfer the + * file. + */ + protected StreamInitiation getStreamInitiation() { + return streamInitiation; + } + + /** + * Accepts this file transfer and creates the incoming file transfer. + * + * @return Returns the <b><i>IncomingFileTransfer</b></i> on which the + * file transfer can be carried out. + */ + public IncomingFileTransfer accept() { + return manager.createIncomingFileTransfer(this); + } + + /** + * Rejects the file transfer request. + */ + public void reject() { + manager.rejectIncomingFileTransfer(this); + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java new file mode 100644 index 000000000..53fed24ee --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java @@ -0,0 +1,457 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.*; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smackx.packet.IBBExtensions; +import org.jivesoftware.smackx.packet.IBBExtensions.Open; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * The in-band bytestream file transfer method, or IBB for short, transfers the + * file over the same XML Stream used by XMPP. It is the fall-back mechanism in + * case the SOCKS5 bytestream method of transfering files is not available. + * + * @author Alexander Wenckus + * @see <a href="http://www.jabber.org/jeps/jep-0047.html">JEP-0047: In-Band + * Bytestreams (IBB)</a> + */ +public class IBBTransferNegotiator extends StreamNegotiator { + + protected static final String NAMESPACE = "http://jabber.org/protocol/ibb"; + + public static final int DEFAULT_BLOCK_SIZE = 4096; + + private XMPPConnection connection; + + /** + * The default constructor for the In-Band Bystream Negotiator. + * + * @param connection The connection which this negotiator works on. + */ + protected IBBTransferNegotiator(XMPPConnection connection) { + this.connection = connection; + } + + public PacketFilter getInitiationPacketFilter(String from, String streamID) { + return new AndFilter(new FromContainsFilter( + from), new IBBOpenSidFilter(streamID)); + } + + InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException { + Open openRequest = (Open) streamInitiation; + + if (openRequest.getType().equals(IQ.Type.ERROR)) { + throw new XMPPException(openRequest.getError()); + } + + PacketFilter dataFilter = new IBBMessageSidFilter(openRequest.getFrom(), + openRequest.getSessionID()); + PacketFilter closeFilter = new AndFilter(new PacketTypeFilter( + IBBExtensions.Close.class), new FromMatchesFilter(openRequest + .getFrom())); + + InputStream stream = new IBBInputStream(openRequest.getSessionID(), + dataFilter, closeFilter); + + initInBandTransfer(openRequest); + + return stream; + } + + public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException { + Packet openRequest = initiateIncomingStream(connection, initiation); + return negotiateIncomingStream(openRequest); + } + + /** + * Creates and sends the response for the open request. + * + * @param openRequest The open request recieved from the peer. + */ + private void initInBandTransfer(final Open openRequest) { + connection.sendPacket(FileTransferNegotiator.createIQ(openRequest + .getPacketID(), openRequest.getFrom(), openRequest.getTo(), + IQ.Type.RESULT)); + } + + public OutputStream createOutgoingStream(String streamID, String initiator, + String target) throws XMPPException { + Open openIQ = new Open(streamID, DEFAULT_BLOCK_SIZE); + openIQ.setTo(target); + openIQ.setType(IQ.Type.SET); + + // wait for the result from the peer + PacketCollector collector = connection + .createPacketCollector(new PacketIDFilter(openIQ.getPacketID())); + connection.sendPacket(openIQ); + + IQ openResponse = (IQ) collector.nextResult(); + collector.cancel(); + + if (openResponse == null) { + throw new XMPPException("No response from peer"); + } + + IQ.Type type = openResponse.getType(); + if (!type.equals(IQ.Type.RESULT)) { + if (type.equals(IQ.Type.ERROR)) { + throw new XMPPException("Target returned an error", + openResponse.getError()); + } + else { + throw new XMPPException("Target returned unknown response"); + } + } + + return new IBBOutputStream(target, streamID, DEFAULT_BLOCK_SIZE); + } + + public String[] getNamespaces() { + return new String[]{NAMESPACE}; + } + + public void cleanup() { + } + + private class IBBOutputStream extends OutputStream { + + protected byte[] buffer; + + protected int count = 0; + + protected int seq = 0; + + final String userID; + + private final int options = Base64.DONT_BREAK_LINES; + + final private IQ closePacket; + + private String messageID; + private String sid; + + IBBOutputStream(String userID, String sid, int blockSize) { + if (blockSize <= 0) { + throw new IllegalArgumentException("Buffer size <= 0"); + } + buffer = new byte[blockSize]; + this.userID = userID; + + Message template = new Message(userID); + messageID = template.getPacketID(); + this.sid = sid; + closePacket = createClosePacket(userID, sid); + } + + private IQ createClosePacket(String userID, String sid) { + IQ packet = new IBBExtensions.Close(sid); + packet.setTo(userID); + packet.setType(IQ.Type.SET); + return packet; + } + + public void write(int b) throws IOException { + if (count >= buffer.length) { + flushBuffer(); + } + + buffer[count++] = (byte) b; + } + + public synchronized void write(byte b[], int off, int len) + throws IOException { + if (len >= buffer.length) { + throw new IllegalArgumentException( + "byte size exceeds blocksize"); + } + if (len > buffer.length - count) { + flushBuffer(); + } + System.arraycopy(b, off, buffer, count, len); + count += len; + } + + private void flushBuffer() { + writeToXML(buffer, 0, count); + + count = 0; + } + + private synchronized void writeToXML(byte[] buffer, int offset, int len) { + Message template = createTemplate(messageID + "_" + seq); + IBBExtensions.Data ext = new IBBExtensions.Data(sid); + template.addExtension(ext); + + String enc = Base64.encodeBytes(buffer, offset, len, options); + + ext.setData(enc); + ext.setSeq(seq); + synchronized(this) { + try { + this.wait(100); + } + catch (InterruptedException e) { + } + } + + connection.sendPacket(template); + + seq = (seq + 1 == 65535 ? 0 : seq + 1); + } + + public void close() throws IOException { + connection.sendPacket(closePacket); + } + + public void flush() throws IOException { + flushBuffer(); + } + + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + public Message createTemplate(String messageID) { + Message template = new Message(userID); + template.setPacketID(messageID); + return template; + } + } + + private class IBBInputStream extends InputStream implements PacketListener { + + private String streamID; + + private PacketCollector dataCollector; + + private byte[] buffer; + + private int bufferPointer; + + private int seq = -1; + + private boolean isDone; + + private boolean isEOF; + + private boolean isClosed; + + private IQ closeConfirmation; + + private Message lastMess; + + private IBBInputStream(String streamID, PacketFilter dataFilter, + PacketFilter closeFilter) { + this.streamID = streamID; + this.dataCollector = connection.createPacketCollector(dataFilter); + connection.addPacketListener(this, closeFilter); + this.bufferPointer = -1; + } + + public synchronized int read() throws IOException { + if (isEOF || isClosed) { + return -1; + } + if (bufferPointer == -1 || bufferPointer >= buffer.length) { + loadBufferWait(); + } + + return (int) buffer[bufferPointer++]; + } + + public synchronized int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + public synchronized int read(byte[] b, int off, int len) + throws IOException { + if (isEOF || isClosed) { + return -1; + } + if (bufferPointer == -1 || bufferPointer >= buffer.length) { + if (!loadBufferWait()) { + isEOF = true; + return -1; + } + } + + if (len - off > buffer.length - bufferPointer) { + len = buffer.length - bufferPointer; + } + + System.arraycopy(buffer, bufferPointer, b, off, len); + bufferPointer += len; + return len; + } + + private boolean loadBufferWait() throws IOException { + IBBExtensions.Data data; + + Message mess = null; + while (mess == null) { + if (isDone) { + mess = (Message) dataCollector.pollResult(); + if (mess == null) { + return false; + } + } + else { + mess = (Message) dataCollector.nextResult(1000); + } + } + lastMess = mess; + data = (IBBExtensions.Data) mess.getExtension( + IBBExtensions.Data.ELEMENT_NAME, + IBBExtensions.NAMESPACE); + + checkSequence(mess, (int) data.getSeq()); + buffer = Base64.decode(data.getData()); + bufferPointer = 0; + return true; + } + + private void checkSequence(Message mess, int seq) throws IOException { + if (this.seq == 65535) { + this.seq = -1; + } + if (seq - 1 != this.seq) { + cancelTransfer(mess); + throw new IOException("Packets out of sequence"); + } + else { + this.seq = seq; + } + } + + private void cancelTransfer(Message mess) { + cleanup(); + + sendCancelMessage(mess); + } + + private void cleanup() { + dataCollector.cancel(); + connection.removePacketListener(this); + } + + private void sendCancelMessage(Message message) { + IQ error = FileTransferNegotiator.createIQ(message.getPacketID(), message.getFrom(), message.getTo(), + IQ.Type.ERROR); + error.setError(new XMPPError(504)); + connection.sendPacket(error); + } + + public boolean markSupported() { + return false; + } + + public void processPacket(Packet packet) { + IBBExtensions.Close close = (IBBExtensions.Close) packet; + if (close.getSessionID().equals(streamID)) { + isDone = true; + closeConfirmation = FileTransferNegotiator.createIQ(packet + .getPacketID(), packet.getFrom(), packet.getTo(), + IQ.Type.RESULT); + } + } + + public synchronized void close() throws IOException { + if (isClosed) { + return; + } + cleanup(); + + if (isEOF) { + sendCloseConfirmation(); + } + else if(lastMess != null) { + sendCancelMessage(lastMess); + } + isClosed = true; + } + + private void sendCloseConfirmation() { + connection.sendPacket(closeConfirmation); + } + } + + private static class IBBOpenSidFilter implements PacketFilter { + + private String sessionID; + + public IBBOpenSidFilter(String sessionID) { + if (sessionID == null) { + throw new IllegalArgumentException("StreamID cannot be null"); + } + this.sessionID = sessionID; + } + + public boolean accept(Packet packet) { + if (!IBBExtensions.Open.class.isInstance(packet)) { + return false; + } + IBBExtensions.Open open = (IBBExtensions.Open) packet; + String sessionID = open.getSessionID(); + + return (sessionID != null && sessionID.equals(this.sessionID)); + } + } + + private static class IBBMessageSidFilter implements PacketFilter { + + private final String sessionID; + private String from; + + public IBBMessageSidFilter(String from, String sessionID) { + this.from = from; + this.sessionID = sessionID; + } + + public boolean accept(Packet packet) { + if (!(packet instanceof Message)) { + return false; + } + if (!packet.getFrom().equalsIgnoreCase(from)) { + return false; + } + + IBBExtensions.Data data = (IBBExtensions.Data) packet. + getExtension(IBBExtensions.Data.ELEMENT_NAME, IBBExtensions.NAMESPACE); + if (data == null) { + return false; + } + return data.getSessionID() != null && data.getSessionID().equalsIgnoreCase(sessionID); + } + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java new file mode 100644 index 000000000..33be034c2 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java @@ -0,0 +1,187 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.XMPPException; + +import java.io.*; + +/** + * An incoming file transfer is created when the + * {@link FileTransferManager#createIncomingFileTransfer(FileTransferRequest)} + * method is invoked. It is a file being sent to the local user from another + * user on the jabber network. There are two stages of the file transfer to be + * concerned with and they can be handled in different ways depending upon the + * method that is invoked on this class. + * <p/> + * The first way that a file is recieved is by calling the + * {@link #recieveFile()} method. This method, negotiates the appropriate stream + * method and then returns the <b><i>InputStream</b></i> to read the file + * data from. + * <p/> + * The second way that a file can be recieved through this class is by invoking + * the {@link #recieveFile(File)} method. This method returns immediatly and + * takes as its parameter a file on the local file system where the file + * recieved from the transfer will be put. + * + * @author Alexander Wenckus + */ +public class IncomingFileTransfer extends FileTransfer { + + private FileTransferRequest recieveRequest; + + private Thread transferThread; + + private InputStream inputStream; + + protected IncomingFileTransfer(FileTransferRequest request, + FileTransferNegotiator transferNegotiator) { + super(request.getRequestor(), request.getStreamID(), transferNegotiator); + this.recieveRequest = request; + } + + /** + * Negotiates the stream method to transfer the file over and then returns + * the negotiated stream. + * + * @return The negotiated InputStream from which to read the data. + * @throws XMPPException If there is an error in the negotiation process an exception + * is thrown. + */ + public InputStream recieveFile() throws XMPPException { + if (inputStream != null) { + throw new IllegalStateException("Transfer already negotiated!"); + } + + try { + inputStream = negotiateStream(); + } + catch (XMPPException e) { + setException(e); + throw e; + } + + return inputStream; + } + + /** + * This method negotitates the stream and then transfer's the file over the + * negotiated stream. The transfered file will be saved at the provided + * location. + * <p/> + * This method will return immedialtly, file transfer progress can be + * monitored through several methods: + * <p/> + * <UL> + * <LI>{@link FileTransfer#getStatus()} + * <LI>{@link FileTransfer#getProgress()} + * <LI>{@link FileTransfer#isDone()} + * </UL> + * + * @param file The location to save the file. + * @throws XMPPException + * @throws IllegalArgumentException This exception is thrown when the the provided file is + * either null, or cannot be written to. + */ + public void recieveFile(final File file) throws XMPPException { + if (file != null) { + if (!file.exists()) { + try { + file.createNewFile(); + } + catch (IOException e) { + throw new XMPPException( + "Could not create file to write too", e); + } + } + if (!file.canWrite()) { + throw new IllegalArgumentException("Cannot write to provided file"); + } + } + else { + throw new IllegalArgumentException("File cannot be null"); + } + + transferThread = new Thread(new Runnable() { + public void run() { + try { + inputStream = negotiateStream(); + } + catch (XMPPException e) { + handleXMPPException(e); + return; + } + + OutputStream outputStream = null; + try { + outputStream = new FileOutputStream(file); + setStatus(Status.IN_PROGRESS); + writeToStream(inputStream, outputStream); + } + catch (XMPPException e) { + setStatus(FileTransfer.Status.ERROR); + setError(Error.STREAM); + setException(e); + } + catch (FileNotFoundException e) { + setStatus(FileTransfer.Status.ERROR); + setError(Error.BAD_FILE); + setException(e); + } + + if (getStatus().equals(Status.IN_PROGRESS)) + setStatus(Status.COMPLETE); + try { + if (inputStream != null) { + inputStream.close(); + } + if (outputStream != null) { + outputStream.close(); + } + } + catch (IOException e) { + } + } + }, "File Transfer " + streamID); + transferThread.start(); + + } + + private void handleXMPPException(XMPPException e) { + setStatus(FileTransfer.Status.ERROR); + setException(e); + } + + private InputStream negotiateStream() throws XMPPException { + setStatus(Status.NEGOTIATING_TRANSFER); + StreamNegotiator streamNegotiator = negotiator + .selectStreamNegotiator(recieveRequest); + setStatus(Status.NEGOTIATING_STREAM); + InputStream inputStream = streamNegotiator + .createIncomingStream(recieveRequest.getStreamInitiation()); + setStatus(Status.NEGOTIATED); + return inputStream; + } + + public void cancel() { + setStatus(Status.CANCLED); + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java new file mode 100644 index 000000000..db0f1c034 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java @@ -0,0 +1,364 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.filetransfer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.XMPPError; + +/** + * Handles the sending of a file to another user. File transfer's in jabber have + * several steps and there are several methods in this class that handle these + * steps differently. + * + * @author Alexander Wenckus + * + */ +public class OutgoingFileTransfer extends FileTransfer { + + private static int RESPONSE_TIMEOUT = 60 * 1000; + + /** + * Returns the time in milliseconds after which the file transfer + * negotiation process will timeout if the other user has not responded. + * + * @return Returns the time in milliseconds after which the file transfer + * negotiation process will timeout if the remote user has not + * responded. + */ + public static int getResponseTimeout() { + return RESPONSE_TIMEOUT; + } + + /** + * Sets the time in milliseconds after which the file transfer negotiation + * process will timeout if the other user has not responded. + * + * @param responseTimeout + * The timeout time in milliseconds. + */ + public void setResponseTimeout(int responseTimeout) { + RESPONSE_TIMEOUT = responseTimeout; + } + + private OutputStream outputStream; + + private String initiator; + + private Thread transferThread; + + protected OutgoingFileTransfer(String initiator, String target, + String streamID, FileTransferNegotiator transferNegotiator) { + super(target, streamID, transferNegotiator); + this.initiator = initiator; + } + + protected void setOutputStream(OutputStream stream) { + if (outputStream == null) { + this.outputStream = stream; + } + } + + /** + * Returns the output stream connected to the peer to transfer the file. It + * is only available after it has been succesfully negotiated by the + * {@link StreamNegotiator}. + * + * @return Returns the output stream connected to the peer to transfer the + * file. + */ + protected OutputStream getOutputStream() { + if (getStatus().equals(FileTransfer.Status.NEGOTIATED)) { + return outputStream; + } else { + return null; + } + } + + /** + * This method handles the negotiation of the file transfer and the stream, + * it only returns the created stream after the negotiation has been completed. + * + * @param fileName + * The name of the file that will be transmitted. It is + * preferable for this name to have an extension as it will be + * used to determine the type of file it is. + * @param fileSize + * The size in bytes of the file that will be transmitted. + * @param description + * A description of the file that will be transmitted. + * @return The OutputStream that is connected to the peer to transmit the + * file. + * @throws XMPPException + * Thrown if an error occurs during the file transfer + * negotiation process. + */ + public synchronized OutputStream sendFile(String fileName, long fileSize, + String description) throws XMPPException { + if (isDone() || outputStream != null) { + throw new IllegalStateException( + "The negotation process has already" + + " been attempted on this file transfer"); + } + try { + this.outputStream = negotiateStream(fileName, fileSize, description); + } catch (XMPPException e) { + handleXMPPException(e); + throw e; + } + return outputStream; + } + + /** + * This methods handles the transfer and stream negotiation process. It + * returns immediately and its progress can be monitored through the + * {@link NegotiationProgress} callback. When the negotiation process is + * complete the OutputStream can be retrieved from the callback via the + * {@link NegotiationProgress#getOutputStream()} method. + * + * @param fileName + * The name of the file that will be transmitted. It is + * preferable for this name to have an extension as it will be + * used to determine the type of file it is. + * @param fileSize + * The size in bytes of the file that will be transmitted. + * @param description + * A description of the file that will be transmitted. + * @param progress + * A callback to monitor the progress of the file transfer + * negotiation process and to retrieve the OutputStream when it + * is complete. + */ + public synchronized void sendFile(final String fileName, + final long fileSize, final String description, + NegotiationProgress progress) { + checkTransferThread(); + if (isDone() || outputStream != null) { + throw new IllegalStateException( + "The negotation process has already" + + " been attempted for this file transfer"); + } + progress.delegate = this; + transferThread = new Thread(new Runnable() { + public void run() { + try { + OutgoingFileTransfer.this.outputStream = negotiateStream( + fileName, fileSize, description); + } catch (XMPPException e) { + handleXMPPException(e); + } + } + }, "File Transfer Negotiation " + streamID); + transferThread.start(); + } + + private void checkTransferThread() { + if (transferThread != null && transferThread.isAlive() || isDone()) { + throw new IllegalStateException( + "File transfer in progress or has already completed."); + } + } + + /** + * This method handles the stream negotiation process and transmits the file + * to the remote user. It returns immediatly and the progress of the file + * transfer can be monitored through several methods: + * + * <UL> + * <LI>{@link FileTransfer#getStatus()} + * <LI>{@link FileTransfer#getProgress()} + * <LI>{@link FileTransfer#isDone()} + * </UL> + * + * @throws XMPPException + * If there is an error during the negotiation process or the + * sending of the file. + */ + public synchronized void sendFile(final File file, final String description) + throws XMPPException { + checkTransferThread(); + if (file == null || !file.exists() || !file.canRead()) { + throw new IllegalArgumentException("Could not read file"); + } else { + setFileInfo(file.getAbsolutePath(), file.getName(), file.length()); + } + + transferThread = new Thread(new Runnable() { + public void run() { + try { + outputStream = negotiateStream(file.getName(), file + .length(), description); + } catch (XMPPException e) { + handleXMPPException(e); + return; + } + if (outputStream == null) { + return; + } + + if (!getStatus().equals(Status.NEGOTIATED)) { + return; + } + setStatus(Status.IN_PROGRESS); + + InputStream inputStream = null; + try { + inputStream = new FileInputStream(file); + writeToStream(inputStream, outputStream); + } catch (FileNotFoundException e) { + setStatus(FileTransfer.Status.ERROR); + setError(Error.BAD_FILE); + setException(e); + } catch (XMPPException e) { + setStatus(FileTransfer.Status.ERROR); + setException(e); + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + + outputStream.flush(); + outputStream.close(); + } catch (IOException e) { + } + } + if (getStatus().equals(Status.IN_PROGRESS)) { + setStatus(FileTransfer.Status.COMPLETE); + } + } + + }, "File Transfer " + streamID); + transferThread.start(); + } + + private void handleXMPPException(XMPPException e) { + setStatus(FileTransfer.Status.ERROR); + XMPPError error = e.getXMPPError(); + if (error != null) { + int code = error.getCode(); + if (code == 403) { + setStatus(Status.REFUSED); + return; + } else if (code == 400) { + setStatus(Status.ERROR); + setError(Error.NOT_ACCEPTABLE); + } + } + setException(e); + return; + } + + /** + * Returns the amount of bytes that have been sent for the file transfer. Or + * -1 if the file transfer has not started. + * <p> + * Note: This method is only useful when the {@link #sendFile(File, String)} + * method is called, as it is the only method that actualy transmits the + * file. + * + * @return Returns the amount of bytes that have been sent for the file + * transfer. Or -1 if the file transfer has not started. + */ + public long getBytesSent() { + return amountWritten; + } + + private OutputStream negotiateStream(String fileName, long fileSize, + String description) throws XMPPException { + // Negotiate the file transfer profile + + setStatus(Status.NEGOTIATING_TRANSFER); + StreamNegotiator streamNegotiator = negotiator.negotiateOutgoingTransfer( + getPeer(), streamID, fileName, fileSize, description, + RESPONSE_TIMEOUT); + + if (streamNegotiator == null) { + setStatus(Status.ERROR); + setError(Error.NO_RESPONSE); + return null; + } + + if (!getStatus().equals(Status.NEGOTIATING_TRANSFER)) { + return null; + } + + // Negotiate the stream + + setStatus(Status.NEGOTIATING_STREAM); + outputStream = streamNegotiator.createOutgoingStream(streamID, + initiator, getPeer()); + if (!getStatus().equals(Status.NEGOTIATING_STREAM)) { + return null; + } + setStatus(Status.NEGOTIATED); + return outputStream; + } + + public void cancel() { + setStatus(Status.CANCLED); + } + + /** + * A callback class to retrive the status of an outgoing transfer + * negotiation process. + * + * @author Alexander Wenckus + * + */ + public static class NegotiationProgress { + + private OutgoingFileTransfer delegate; + + /** + * Returns the current status of the negotiation process. + * + * @return Returns the current status of the negotiation process. + */ + public Status getStatus() { + if (delegate == null) { + throw new IllegalStateException("delegate not yet set"); + } + return delegate.getStatus(); + } + + /** + * Once the negotiation process is completed the output stream can be + * retrieved. + * + * @return Once the negotiation process is completed the output stream + * can be retrieved. + * + */ + public OutputStream getOutputStream() { + if (delegate == null) { + throw new IllegalStateException("delegate not yet set"); + } + return delegate.getOutputStream(); + } + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java new file mode 100644 index 000000000..1252fb3c7 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java @@ -0,0 +1,777 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.FromMatchesFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.Bytestream; +import org.jivesoftware.smackx.packet.Bytestream.StreamHost; +import org.jivesoftware.smackx.packet.Bytestream.StreamHostUsed; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverInfo.Identity; +import org.jivesoftware.smackx.packet.DiscoverItems; +import org.jivesoftware.smackx.packet.DiscoverItems.Item; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.io.*; +import java.net.*; +import java.util.*; + +/** + * A SOCKS5 bytestream is negotiated partly over the XMPP XML stream and partly + * over a seperate socket. The actual transfer though takes place over a + * seperatly created socket. + * <p/> + * A SOCKS5 file transfer generally has three parites, the initiator, the + * target, and the stream host. The stream host is a specialized SOCKS5 proxy + * setup on the server, or, the Initiator can act as the Stream Host if the + * proxy is not available. + * <p/> + * The advantage of having a seperate proxy over directly connecting to + * eachother is if the Initator and the Target are not on the same LAN and are + * operating behind NAT, the proxy allows for a common location for both parties + * to connect to and transfer the file. + * <p/> + * Smack will attempt to automatically discover any proxies present on your + * server. If any are detected they will be forwarded to any user attempting to + * recieve files from you. + * + * @author Alexander Wenckus + * @see <a href="http://www.jabber.org/jeps/jep-0065.html">JEP-0065: SOCKS5 + * Bytestreams</a> + */ +public class Socks5TransferNegotiator extends StreamNegotiator { + + protected static final String NAMESPACE = "http://jabber.org/protocol/bytestreams"; + + public static boolean isAllowLocalProxyHost = true; + + private final XMPPConnection connection; + + private List proxies; + + private List streamHosts; + + // locks the proxies during their initialization process + private final Object proxyLock = new Object(); + + private ProxyProcess proxyProcess; + + // locks on the proxy process during its initiatilization process + private final Object processLock = new Object(); + + public Socks5TransferNegotiator(final XMPPConnection connection) { + this.connection = connection; + } + + public PacketFilter getInitiationPacketFilter(String from, String sessionID) { + return new AndFilter(new FromMatchesFilter(from), + new BytestreamSIDFilter(sessionID)); + } + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateDownload(org.jivesoftware.smackx.packet.StreamInitiation, + * java.io.File) + */ + InputStream negotiateIncomingStream(Packet streamInitiation) + throws XMPPException { + + Bytestream streamHostsInfo = (Bytestream) streamInitiation; + + if (streamHostsInfo.getType().equals(IQ.Type.ERROR)) { + throw new XMPPException(streamHostsInfo.getError()); + } + SelectedHostInfo selectedHost; + try { + // select appropriate host + selectedHost = selectHost(streamHostsInfo); + } + catch (XMPPException ex) { + if (ex.getXMPPError() != null) { + IQ errorPacket = super.createError(streamHostsInfo.getTo(), + streamHostsInfo.getFrom(), streamHostsInfo.getPacketID(), + ex.getXMPPError()); + connection.sendPacket(errorPacket); + } + throw(ex); + } + + // send used-host confirmation + Bytestream streamResponse = createUsedHostConfirmation( + selectedHost.selectedHost, streamHostsInfo.getFrom(), + streamHostsInfo.getTo(), streamHostsInfo.getPacketID()); + connection.sendPacket(streamResponse); + + try { + return selectedHost.establishedSocket.getInputStream(); + } + catch (IOException e) { + throw new XMPPException("Error establishing input stream", e); + } + + } + + public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException { + Packet streamInitiation = initiateIncomingStream(connection, initiation); + return negotiateIncomingStream(streamInitiation); + } + + /** + * The used host confirmation is sent to the initiator to indicate to them + * which of the hosts they provided has been selected and successfully + * connected to. + * + * @param selectedHost The selected stream host. + * @param initiator The initiator of the stream. + * @param target The target of the stream. + * @param packetID The of the packet being responded to. + * @return The packet that was created to send to the initiator. + */ + private Bytestream createUsedHostConfirmation(StreamHost selectedHost, + String initiator, String target, String packetID) { + Bytestream streamResponse = new Bytestream(); + streamResponse.setTo(initiator); + streamResponse.setFrom(target); + streamResponse.setType(IQ.Type.RESULT); + streamResponse.setPacketID(packetID); + streamResponse.setUsedHost(selectedHost.getJID()); + return streamResponse; + } + + /** + * @param streamHostsInfo + * @return + * @throws XMPPException + */ + private SelectedHostInfo selectHost(Bytestream streamHostsInfo) + throws XMPPException { + Iterator it = streamHostsInfo.getStreamHosts().iterator(); + StreamHost selectedHost = null; + Socket socket = null; + while (it.hasNext()) { + selectedHost = (StreamHost) it.next(); + + // establish socket + try { + socket = new Socket(selectedHost.getAddress(), selectedHost + .getPort()); + establishSOCKS5ConnectionToProxy(socket, createDigest( + streamHostsInfo.getSessionID(), streamHostsInfo + .getFrom(), streamHostsInfo.getTo())); + break; + } + catch (IOException e) { + e.printStackTrace(); + selectedHost = null; + socket = null; + } + } + if (selectedHost == null || socket == null) { + throw new XMPPException( + "Could not establish socket with any provided host", new XMPPError(406)); + } + + return new SelectedHostInfo(selectedHost, socket); + } + + /** + * Creates the digest needed for a byte stream. It is the SHA1(sessionID + + * initiator + target). + * + * @param sessionID The sessionID of the stream negotiation + * @param initiator The inititator of the stream negotiation + * @param target The target of the stream negotiation + * @return SHA-1 hash of the three parameters + */ + private String createDigest(final String sessionID, final String initiator, + final String target) { + return StringUtils.hash(sessionID + StringUtils.parseName(initiator) + + "@" + StringUtils.parseServer(initiator) + "/" + + StringUtils.parseResource(initiator) + + StringUtils.parseName(target) + "@" + + StringUtils.parseServer(target) + "/" + + StringUtils.parseResource(target)); + } + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smackx.filetransfer.StreamNegotiator#initiateUpload(java.lang.String, + * org.jivesoftware.smackx.packet.StreamInitiation, java.io.File) + */ + public OutputStream createOutgoingStream(String streamID, String initiator, + String target) throws XMPPException { + Socket socket; + try { + socket = initBytestreamSocket(streamID, initiator, target); + } + catch (Exception e) { + throw new XMPPException("Error establishing transfer socket", e); + } + + if (socket != null) { + try { + return socket.getOutputStream(); + } + catch (IOException e) { + throw new XMPPException("Error establishing output stream", e); + } + } + return null; + } + + private Socket initBytestreamSocket(final String sessionID, + String initiator, String target) throws Exception { + ProxyProcess process; + try { + process = establishListeningSocket(); + } + catch (IOException io) { + process = null; + } + + String localIP; + try { + localIP = discoverLocalIP(); + } + catch (UnknownHostException e1) { + localIP = null; + } + + Bytestream query = createByteStreamInit(initiator, target, sessionID, + localIP, (process != null ? process.getPort() : 0)); + + // if the local host is one of the options we need to wait for the + // remote connection. + Socket conn = waitForUsedHostResponse(sessionID, process, createDigest( + sessionID, initiator, target), query).establishedSocket; + cleanupListeningSocket(); + return conn; + } + + + /** + * Waits for the peer to respond with which host they chose to use. + * + * @param sessionID The session id of the stream. + * @param proxy The server socket which will listen locally for remote + * connections. + * @param digest + * @param query + * @return + * @throws XMPPException + * @throws IOException + */ + private SelectedHostInfo waitForUsedHostResponse(String sessionID, + final ProxyProcess proxy, final String digest, + final Bytestream query) throws XMPPException, IOException { + SelectedHostInfo info = new SelectedHostInfo(); + + PacketCollector collector = connection + .createPacketCollector(new PacketIDFilter(query.getPacketID())); + connection.sendPacket(query); + + Packet packet = collector.nextResult(); + collector.cancel(); + Bytestream response; + if (packet instanceof Bytestream) { + response = (Bytestream) packet; + } + else { + throw new XMPPException("Unexpected response from remote user"); + } + + // check for an error + if (response.getType().equals(IQ.Type.ERROR)) { + throw new XMPPException("Remote client returned error, stream hosts expected", + response.getError()); + } + + StreamHostUsed used = response.getUsedHost(); + StreamHost usedHost = query.getStreamHost(used.getJID()); + if (usedHost == null) { + throw new XMPPException("Remote user responded with unknown host"); + } + // The local computer is acting as the proxy + if (used.getJID().equals(query.getFrom())) { + info.establishedSocket = proxy.getSocket(digest); + info.selectedHost = usedHost; + return info; + } + else { + info.establishedSocket = new Socket(usedHost.getAddress(), usedHost + .getPort()); + establishSOCKS5ConnectionToProxy(info.establishedSocket, digest); + + Bytestream activate = createByteStreamActivate(sessionID, response + .getTo(), usedHost.getJID(), response.getFrom()); + + collector = connection.createPacketCollector(new PacketIDFilter( + activate.getPacketID())); + connection.sendPacket(activate); + + IQ serverResponse = (IQ) collector.nextResult(); + collector.cancel(); + if (!serverResponse.getType().equals(IQ.Type.RESULT)) { + info.establishedSocket.close(); + return null; + } + return info; + } + } + + private ProxyProcess establishListeningSocket() throws IOException { + synchronized (processLock) { + if (proxyProcess == null) { + proxyProcess = new ProxyProcess(new ServerSocket(7777)); + proxyProcess.start(); + } + } + proxyProcess.addTransfer(); + return proxyProcess; + } + + private void cleanupListeningSocket() { + if (proxyProcess == null) { + return; + } + proxyProcess.removeTransfer(); + } + + private String discoverLocalIP() throws UnknownHostException { + return InetAddress.getLocalHost().getHostAddress(); + } + + /** + * The bytestream init looks like this: + * <p/> + * <pre> + * <iq type='set' + * from='initiator@host1/foo' + * to='target@host2/bar' + * id='initiate'> + * <query xmlns='http://jabber.org/protocol/bytestreams' + * sid='mySID' + * mode='tcp'> + * <streamhost + * jid='initiator@host1/foo' + * host='192.168.4.1' + * port='5086'/> + * <streamhost + * jid='proxy.host3' + * host='24.24.24.1' + * zeroconf='_jabber.bytestreams'/> + * </query> + * </iq> + * </pre> + * + * @param from initiator@host1/foo - The file transfer initiator. + * @param to target@host2/bar - The file transfer target. + * @param sid 'mySID' - the unique identifier for this file transfer + * @param localIP The IP of the local machine if it is being provided, null otherwise. + * @param port The port of the local mahine if it is being provided, null otherwise. + * @return Returns the created <b><i>Bytestream</b></i> packet + */ + private Bytestream createByteStreamInit(final String from, final String to, + final String sid, final String localIP, final int port) { + Bytestream bs = new Bytestream(); + bs.setTo(to); + bs.setFrom(from); + bs.setSessionID(sid); + bs.setType(IQ.Type.SET); + bs.setMode(Bytestream.Mode.TCP); + if (localIP != null && port > 0) { + bs.addStreamHost(from, localIP, port); + } + // make sure the proxies have been initialized completely + synchronized (proxyLock) { + if (proxies == null) { + initProxies(); + } + } + if (streamHosts != null) { + Iterator it = streamHosts.iterator(); + while (it.hasNext()) { + bs.addStreamHost((StreamHost) it.next()); + } + } + + return bs; + } + + private void initProxies() { + proxies = new ArrayList(); + ServiceDiscoveryManager manager = ServiceDiscoveryManager + .getInstanceFor(connection); + + DiscoverItems discoItems; + try { + discoItems = manager.discoverItems(connection.getServiceName()); + + DiscoverItems.Item item; + DiscoverInfo info; + DiscoverInfo.Identity identity; + + Iterator it = discoItems.getItems(); + while (it.hasNext()) { + item = (Item) it.next(); + info = manager.discoverInfo(item.getEntityID()); + Iterator itx = info.getIdentities(); + while (itx.hasNext()) { + identity = (Identity) itx.next(); + if (identity.getCategory().equalsIgnoreCase("proxy") + && identity.getType().equalsIgnoreCase( + "bytestreams")) { + proxies.add(info.getFrom()); + } + } + } + } + catch (XMPPException e) { + return; + } + if (proxies.size() > 0) { + initStreamHosts(); + } + + } + + private void initStreamHosts() { + List streamHosts = new ArrayList(); + Iterator it = proxies.iterator(); + IQ query; + PacketCollector collector; + Bytestream response; + while (it.hasNext()) { + String jid = it.next().toString(); + query = new IQ() { + public String getChildElementXML() { + return "<query xmlns=\"http://jabber.org/protocol/bytestreams\"/>"; + } + }; + query.setType(IQ.Type.GET); + query.setTo(jid); + + collector = connection.createPacketCollector(new PacketIDFilter( + query.getPacketID())); + connection.sendPacket(query); + + response = (Bytestream) collector.nextResult(SmackConfiguration + .getPacketReplyTimeout()); + if (response != null) { + streamHosts.addAll(response.getStreamHosts()); + } + collector.cancel(); + } + this.streamHosts = streamHosts; + } + + /** + * Returns the packet to send notification to the stream host to activate + * the stream. + * + * @param sessionID The session ID of the file transfer to activate. + * @param from + * @param to The JID of the stream host + * @param target The JID of the file transfer target. + * @return Returns the packet to send notification to the stream host to + * activate the stream. + */ + private static Bytestream createByteStreamActivate(final String sessionID, + final String from, final String to, final String target) { + Bytestream activate = new Bytestream(sessionID); + activate.setMode(null); + activate.setToActivate(target); + activate.setFrom(from); + activate.setTo(to); + activate.setType(IQ.Type.SET); + return activate; + } + + /** + * Negotiates the Socks 5 bytestream when the local computer is acting as + * the proxy. + * + * @param connection The socket connection with the peer. + * @return The SHA-1 digest that is used to uniquely identify the file + * transfer. + * @throws XMPPException + * @throws IOException + */ + private String establishSocks5UploadConnection(Socket connection) throws XMPPException, IOException { + OutputStream out = new DataOutputStream(connection.getOutputStream()); + InputStream in = new DataInputStream(connection.getInputStream()); + + // first byte is version should be 5 + int b = in.read(); + if (b != 5) { + throw new XMPPException("Only SOCKS5 supported"); + } + + // second byte number of authentication methods supported + b = in.read(); + int[] auth = new int[b]; + for (int i = 0; i < b; i++) { + auth[i] = in.read(); + } + + int authMethod = -1; + for (int i = 0; i < auth.length; i++) { + authMethod = (auth[i] == 0 ? 0 : -1); // only auth method + // 0, no + // authentication, + // supported + if (authMethod == 0) { + break; + } + } + if (authMethod != 0) { + throw new XMPPException("Authentication method not supported"); + } + byte[] cmd = new byte[2]; + cmd[0] = (byte) 0x05; + cmd[1] = (byte) 0x00; + out.write(cmd); + + String responseDigest = createIncomingSocks5Message(in); + cmd = createOutgoingSocks5Message(0, responseDigest); + + if (!connection.isConnected()) { + throw new XMPPException("Socket closed by remote user"); + } + out.write(cmd); + return responseDigest; + } + + public String[] getNamespaces() { + return new String[]{NAMESPACE}; + } + + private void establishSOCKS5ConnectionToProxy(Socket socket, String digest) + throws IOException { + + byte[] cmd = new byte[3]; + + cmd[0] = (byte) 0x05; + cmd[1] = (byte) 0x01; + cmd[2] = (byte) 0x00; + + OutputStream out = new DataOutputStream(socket.getOutputStream()); + out.write(cmd); + + InputStream in = new DataInputStream(socket.getInputStream()); + byte[] response = new byte[2]; + + in.read(response); + + cmd = createOutgoingSocks5Message(1, digest); + out.write(cmd); + createIncomingSocks5Message(in); + } + + private String createIncomingSocks5Message(InputStream in) + throws IOException { + byte[] cmd = new byte[5]; + in.read(cmd, 0, 5); + + byte[] addr = new byte[cmd[4]]; + in.read(addr, 0, addr.length); + String digest = new String(addr); + in.read(); + in.read(); + + return digest; + } + + private byte[] createOutgoingSocks5Message(int cmd, String digest) { + byte addr[] = digest.getBytes(); + + byte[] data = new byte[7 + addr.length]; + data[0] = (byte) 5; + data[1] = (byte) cmd; + data[2] = (byte) 0; + data[3] = (byte) 0x3; + data[4] = (byte) addr.length; + + System.arraycopy(addr, 0, data, 5, addr.length); + data[data.length - 2] = (byte) 0; + data[data.length - 1] = (byte) 0; + + return data; + } + + public void cleanup() { + } + + private static class SelectedHostInfo { + + protected XMPPException exception; + + protected StreamHost selectedHost; + + protected Socket establishedSocket; + + SelectedHostInfo(StreamHost selectedHost, Socket establishedSocket) { + this.selectedHost = selectedHost; + this.establishedSocket = establishedSocket; + } + + public SelectedHostInfo() { + } + } + + private class ProxyProcess implements Runnable { + + private ServerSocket listeningSocket; + + private Map connectionMap = new HashMap(); + + private boolean done = false; + + private Thread thread; + private int transfers; + + public void run() { + try { + listeningSocket.setSoTimeout(10000); + } + catch (SocketException e) { + e.printStackTrace(); + } + while (!done) { + Socket conn = null; + synchronized (ProxyProcess.this) { + while (transfers <= 0) { + transfers = -1; + try { + ProxyProcess.this.wait(); + } + catch (InterruptedException e) { + } + } + } + try { + synchronized (listeningSocket) { + conn = listeningSocket.accept(); + } + if (conn == null) { + continue; + } + String digest = establishSocks5UploadConnection(conn); + synchronized (connectionMap) { + connectionMap.put(digest, conn); + } + } + catch (IOException e) { + } + catch (XMPPException e) { + e.printStackTrace(); + if (conn != null) { + try { + conn.close(); + } + catch (IOException e1) { + } + } + } + } + } + + + public void start() { + thread.start(); + } + + public void stop() { + done = true; + synchronized (this) { + this.notify(); + } + } + + public int getPort() { + return listeningSocket.getLocalPort(); + } + + ProxyProcess(ServerSocket listeningSocket) { + thread = new Thread(this, "File Transfer Connection Listener"); + this.listeningSocket = listeningSocket; + } + + public Socket getSocket(String digest) { + synchronized (connectionMap) { + return (Socket) connectionMap.get(digest); + } + } + + public void addTransfer() { + synchronized (this) { + if (transfers == -1) { + transfers = 1; + this.notify(); + } + else { + transfers++; + } + } + } + + public void removeTransfer() { + synchronized (this) { + transfers--; + } + } + } + + private static class BytestreamSIDFilter implements PacketFilter { + + private String sessionID; + + public BytestreamSIDFilter(String sessionID) { + if (sessionID == null) { + throw new IllegalArgumentException("StreamID cannot be null"); + } + this.sessionID = sessionID; + } + + public boolean accept(Packet packet) { + if (!Bytestream.class.isInstance(packet)) { + return false; + } + Bytestream bytestream = (Bytestream) packet; + String sessionID = bytestream.getSessionID(); + + return (sessionID != null && sessionID.equals(this.sessionID)); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java new file mode 100644 index 000000000..eca2927e2 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java @@ -0,0 +1,164 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.packet.DataForm; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * After the file transfer negotiation process is completed according to + * JEP-0096, the negotation process is passed off to a particular stream + * negotiator. The stream negotiator will then negotiate the chosen stream and + * return the stream to transfer the file. + * + * @author Alexander Wenckus + */ +public abstract class StreamNegotiator { + + /** + * Creates the initiation acceptance packet to forward to the stream + * initiator. + * + * @param streamInitiationOffer The offer from the stream initatior to connect for a stream. + * @param namespaces The namespace that relates to the accepted means of transfer. + * @return The response to be forwarded to the initator. + */ + public StreamInitiation createInitiationAccept( + StreamInitiation streamInitiationOffer, String [] namespaces) { + StreamInitiation response = new StreamInitiation(); + response.setTo(streamInitiationOffer.getFrom()); + response.setFrom(streamInitiationOffer.getTo()); + response.setType(IQ.Type.RESULT); + response.setPacketID(streamInitiationOffer.getPacketID()); + + DataForm form = new DataForm(Form.TYPE_SUBMIT); + FormField field = new FormField( + FileTransferNegotiator.STREAM_DATA_FIELD_NAME); + for (int i = 0; i < namespaces.length; i++) { + field.addValue(namespaces[i]); + } + form.addField(field); + + response.setFeatureNegotiationForm(form); + return response; + } + + + public IQ createError(String from, String to, String packetID, XMPPError xmppError) { + IQ iq = FileTransferNegotiator.createIQ(packetID, to, from, IQ.Type.ERROR); + iq.setError(xmppError); + return iq; + } + + Packet initiateIncomingStream(XMPPConnection connection, StreamInitiation initiation) throws XMPPException { + StreamInitiation response = createInitiationAccept(initiation, + getNamespaces()); + + // establish collector to await response + PacketCollector collector = connection + .createPacketCollector(getInitiationPacketFilter(initiation.getFrom(), initiation.getSessionID())); + connection.sendPacket(response); + + Packet streamMethodInitiation = collector + .nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (streamMethodInitiation == null) { + throw new XMPPException("No response from file transfer initiator"); + } + + return streamMethodInitiation; + } + + /** + * Returns the packet filter that will return the initiation packet for the appropriate stream + * initiation. + * + * @param from The initiatior of the file transfer. + * @param streamID The stream ID related to the transfer. + * @return The <b><i>PacketFilter</b></i> that will return the packet relatable to the stream + * initiation. + */ + public abstract PacketFilter getInitiationPacketFilter(String from, String streamID); + + + abstract InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException; + + /** + * This method handles the file stream download negotiation process. The + * appropriate stream negotiator's initiate incoming stream is called after + * an appropriate file transfer method is selected. The manager will respond + * to the initatior with the selected means of transfer, then it will handle + * any negotation specific to the particular transfer method. This method + * returns the InputStream, ready to transfer the file. + * + * @param initiation The initation that triggered this download. + * @return After the negotation process is complete, the InputStream to + * write a file to is returned. + * @throws XMPPException If an error occurs during this process an XMPPException is + * thrown. + */ + public abstract InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException; + + /** + * This method handles the file upload stream negotiation process. The + * particular stream negotiator is determined during the file transfer + * negotiation process. This method returns the OutputStream to transmit the + * file to the remote user. + * + * @param streamID The streamID that uniquely identifies the file transfer. + * @param initiator The fully-qualified JID of the initiator of the file transfer. + * @param target The fully-qualified JID of the target or reciever of the file + * transfer. + * @return The negotiated stream ready for data. + * @throws XMPPException If an error occurs during the negotiation process an + * exception will be thrown. + */ + public abstract OutputStream createOutgoingStream(String streamID, + String initiator, String target) throws XMPPException; + + /** + * Returns the XMPP namespace reserved for this particular type of file + * transfer. + * + * @return Returns the XMPP namespace reserved for this particular type of + * file transfer. + */ + public abstract String[] getNamespaces(); + + /** + * Cleanup any and all resources associated with this negotiator. + */ + public abstract void cleanup(); + + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Affiliate.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Affiliate.java new file mode 100644 index 000000000..9300dc6f7 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Affiliate.java @@ -0,0 +1,98 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +import org.jivesoftware.smackx.packet.MUCAdmin; +import org.jivesoftware.smackx.packet.MUCOwner; + +/** + * Represents an affiliation of a user to a given room. The affiliate's information will always have + * the bare jid of the real user and its affiliation. If the affiliate is an occupant of the room + * then we will also have information about the role and nickname of the user in the room. + * + * @author Gaston Dombiak + */ +public class Affiliate { + // Fields that must have a value + private String jid; + private String affiliation; + + // Fields that may have a value + private String role; + private String nick; + + Affiliate(MUCOwner.Item item) { + super(); + this.jid = item.getJid(); + this.affiliation = item.getAffiliation(); + this.role = item.getRole(); + this.nick = item.getNick(); + } + + Affiliate(MUCAdmin.Item item) { + super(); + this.jid = item.getJid(); + this.affiliation = item.getAffiliation(); + this.role = item.getRole(); + this.nick = item.getNick(); + } + + /** + * Returns the bare JID of the affiliated user. This information will always be available. + * + * @return the bare JID of the affiliated user. + */ + public String getJid() { + return jid; + } + + /** + * Returns the affiliation of the afffiliated user. Possible affiliations are: "owner", "admin", + * "member", "outcast". This information will always be available. + * + * @return the affiliation of the afffiliated user. + */ + public String getAffiliation() { + return affiliation; + } + + /** + * Returns the current role of the affiliated user if the user is currently in the room. + * If the user is not present in the room then the answer will be null. + * + * @return the current role of the affiliated user in the room or null if the user is not in + * the room. + */ + public String getRole() { + return role; + } + + /** + * Returns the current nickname of the affiliated user if the user is currently in the room. + * If the user is not present in the room then the answer will be null. + * + * @return the current nickname of the affiliated user in the room or null if the user is not in + * the room. + */ + public String getNick() { + return nick; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DeafOccupantInterceptor.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DeafOccupantInterceptor.java new file mode 100644 index 000000000..6d87bb56d --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DeafOccupantInterceptor.java @@ -0,0 +1,76 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +import org.jivesoftware.smack.PacketInterceptor; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.packet.Presence; + +/** + * Packet interceptor that will intercept presence packets sent to the MUC service to indicate + * that the user wants to be a deaf occupant. A user can only indicate that he wants to be a + * deaf occupant while joining the room. It is not possible to become deaf or stop being deaf + * after the user joined the room.<p> + * + * Deaf occupants will not get messages broadcasted to all room occupants. However, they will + * be able to get private messages, presences, IQ packets or room history. To use this + * functionality you will need to send the message + * {@link MultiUserChat#addPresenceInterceptor(org.jivesoftware.smack.PacketInterceptor)} and + * pass this interceptor as the parameter.<p> + * + * Note that this is a custom extension to the MUC service so it may not work with other servers + * than Wildfire. + * + * @author Gaston Dombiak + */ +public class DeafOccupantInterceptor implements PacketInterceptor { + + public void interceptPacket(Packet packet) { + Presence presence = (Presence) packet; + // Check if user is joining a room + if (Presence.Type.AVAILABLE == presence.getType() && + presence.getExtension("x", "http://jabber.org/protocol/muc") != null) { + // Add extension that indicates that user wants to be a deaf occupant + packet.addExtension(new DeafExtension()); + } + } + + private static class DeafExtension implements PacketExtension { + + public String getElementName() { + return "x"; + } + + public String getNamespace() { + return "http://jivesoftware.org/protocol/muc"; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()) + .append("\">"); + buf.append("<deaf-occupant/>"); + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java new file mode 100644 index 000000000..5974710e6 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java @@ -0,0 +1,79 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +/** + * Default implementation of the ParticipantStatusListener interface.<p> + * + * This class does not provide any behavior by default. It just avoids having + * to implement all the inteface methods if the user is only interested in implementing + * some of the methods. + * + * @author Gaston Dombiak + */ +public class DefaultParticipantStatusListener implements ParticipantStatusListener { + + public void joined(String participant) { + } + + public void left(String participant) { + } + + public void kicked(String participant, String actor, String reason) { + } + + public void voiceGranted(String participant) { + } + + public void voiceRevoked(String participant) { + } + + public void banned(String participant, String actor, String reason) { + } + + public void membershipGranted(String participant) { + } + + public void membershipRevoked(String participant) { + } + + public void moderatorGranted(String participant) { + } + + public void moderatorRevoked(String participant) { + } + + public void ownershipGranted(String participant) { + } + + public void ownershipRevoked(String participant) { + } + + public void adminGranted(String participant) { + } + + public void adminRevoked(String participant) { + } + + public void nicknameChanged(String participant, String newNickname) { + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java new file mode 100644 index 000000000..1075d6ca0 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java @@ -0,0 +1,70 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +/** + * Default implementation of the UserStatusListener interface.<p> + * + * This class does not provide any behavior by default. It just avoids having + * to implement all the inteface methods if the user is only interested in implementing + * some of the methods. + * + * @author Gaston Dombiak + */ +public class DefaultUserStatusListener implements UserStatusListener { + + public void kicked(String actor, String reason) { + } + + public void voiceGranted() { + } + + public void voiceRevoked() { + } + + public void banned(String actor, String reason) { + } + + public void membershipGranted() { + } + + public void membershipRevoked() { + } + + public void moderatorGranted() { + } + + public void moderatorRevoked() { + } + + public void ownershipGranted() { + } + + public void ownershipRevoked() { + } + + public void adminGranted() { + } + + public void adminRevoked() { + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DiscussionHistory.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DiscussionHistory.java new file mode 100644 index 000000000..a5edc6fc8 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/DiscussionHistory.java @@ -0,0 +1,173 @@ +/** + * $RCSfile$ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +import java.util.Date; + +import org.jivesoftware.smackx.packet.MUCInitialPresence; + +/** + * The DiscussionHistory class controls the number of characters or messages to receive + * when entering a room. The room will decide the amount of history to return if you don't + * specify a DiscussionHistory while joining a room.<p> + * + * You can use some or all of these variable to control the amount of history to receive: + * <ul> + * <li>maxchars -> total number of characters to receive in the history. + * <li>maxstanzas -> total number of messages to receive in the history. + * <li>seconds -> only the messages received in the last "X" seconds will be included in the + * history. + * <li>since -> only the messages received since the datetime specified will be included in + * the history. + * </ul> + * + * Note: Setting maxchars to 0 indicates that the user requests to receive no history. + * + * @author Gaston Dombiak + */ +public class DiscussionHistory { + + private int maxChars = -1; + private int maxStanzas = -1; + private int seconds = -1; + private Date since; + + /** + * Returns the total number of characters to receive in the history. + * + * @return total number of characters to receive in the history. + */ + public int getMaxChars() { + return maxChars; + } + + /** + * Returns the total number of messages to receive in the history. + * + * @return the total number of messages to receive in the history. + */ + public int getMaxStanzas() { + return maxStanzas; + } + + /** + * Returns the number of seconds to use to filter the messages received during that time. + * In other words, only the messages received in the last "X" seconds will be included in + * the history. + * + * @return the number of seconds to use to filter the messages received during that time. + */ + public int getSeconds() { + return seconds; + } + + /** + * Returns the since date to use to filter the messages received during that time. + * In other words, only the messages received since the datetime specified will be + * included in the history. + * + * @return the since date to use to filter the messages received during that time. + */ + public Date getSince() { + return since; + } + + /** + * Sets the total number of characters to receive in the history. + * + * @param maxChars the total number of characters to receive in the history. + */ + public void setMaxChars(int maxChars) { + this.maxChars = maxChars; + } + + /** + * Sets the total number of messages to receive in the history. + * + * @param maxStanzas the total number of messages to receive in the history. + */ + public void setMaxStanzas(int maxStanzas) { + this.maxStanzas = maxStanzas; + } + + /** + * Sets the number of seconds to use to filter the messages received during that time. + * In other words, only the messages received in the last "X" seconds will be included in + * the history. + * + * @param seconds the number of seconds to use to filter the messages received during + * that time. + */ + public void setSeconds(int seconds) { + this.seconds = seconds; + } + + /** + * Sets the since date to use to filter the messages received during that time. + * In other words, only the messages received since the datetime specified will be + * included in the history. + * + * @param since the since date to use to filter the messages received during that time. + */ + public void setSince(Date since) { + this.since = since; + } + + /** + * Returns true if the history has been configured with some values. + * + * @return true if the history has been configured with some values. + */ + private boolean isConfigured() { + return maxChars > -1 || maxStanzas > -1 || seconds > -1 || since != null; + } + + /** + * Returns the History that manages the amount of discussion history provided on entering a + * room. + * + * @return the History that manages the amount of discussion history provided on entering a + * room. + */ + MUCInitialPresence.History getMUCHistory() { + // Return null if the history was not properly configured + if (!isConfigured()) { + return null; + } + + MUCInitialPresence.History mucHistory = new MUCInitialPresence.History(); + if (maxChars > -1) { + mucHistory.setMaxChars(maxChars); + } + if (maxStanzas > -1) { + mucHistory.setMaxStanzas(maxStanzas); + } + if (seconds > -1) { + mucHistory.setSeconds(seconds); + } + if (since != null) { + mucHistory.setSince(since); + } + return mucHistory; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/HostedRoom.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/HostedRoom.java new file mode 100644 index 000000000..51905a346 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/HostedRoom.java @@ -0,0 +1,65 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +import org.jivesoftware.smackx.packet.DiscoverItems; + +/** + * Hosted rooms by a chat service may be discovered if they are configured to appear in the room + * directory . The information that may be discovered is the XMPP address of the room and the room + * name. The address of the room may be used for obtaining more detailed information + * {@link org.jivesoftware.smackx.muc.MultiUserChat#getRoomInfo(org.jivesoftware.smack.XMPPConnection, String)} + * or could be used for joining the room + * {@link org.jivesoftware.smackx.muc.MultiUserChat#MultiUserChat(org.jivesoftware.smack.XMPPConnection, String)} + * and {@link org.jivesoftware.smackx.muc.MultiUserChat#join(String)}. + * + * @author Gaston Dombiak + */ +public class HostedRoom { + + private String jid; + + private String name; + + public HostedRoom(DiscoverItems.Item item) { + super(); + jid = item.getEntityID(); + name = item.getName(); + } + + /** + * Returns the XMPP address of the hosted room by the chat service. This address may be used + * when creating a <code>MultiUserChat</code> when joining a room. + * + * @return the XMPP address of the hosted room by the chat service. + */ + public String getJid() { + return jid; + } + + /** + * Returns the name of the room. + * + * @return the name of the room. + */ + public String getName() { + return name; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationListener.java new file mode 100644 index 000000000..eeb814e7a --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationListener.java @@ -0,0 +1,49 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.packet.Message; + +/** + * A listener that is fired anytime an invitation to join a MUC room is received. + * + * @author Gaston Dombiak + */ +public interface InvitationListener { + + /** + * Called when the an invitation to join a MUC room is received.<p> + * + * If the room is password-protected, the invitee will receive a password to use to join + * the room. If the room is members-only, the the invitee may be added to the member list. + * + * @param conn the XMPPConnection that received the invitation. + * @param room the room that invitation refers to. + * @param inviter the inviter that sent the invitation. (e.g. crone1@shakespeare.lit). + * @param reason the reason why the inviter sent the invitation. + * @param password the password to use when joining the room. + * @param message the message used by the inviter to send the invitation. + */ + public abstract void invitationReceived(XMPPConnection conn, String room, String inviter, String reason, + String password, Message message); + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationRejectionListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationRejectionListener.java new file mode 100644 index 000000000..81ae0ab55 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/InvitationRejectionListener.java @@ -0,0 +1,38 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +/** + * A listener that is fired anytime an invitee declines or rejects an invitation. + * + * @author Gaston Dombiak + */ +public interface InvitationRejectionListener { + + /** + * Called when the invitee declines the invitation. + * + * @param invitee the invitee that declined the invitation. (e.g. hecate@shakespeare.lit). + * @param reason the reason why the invitee declined the invitation. + */ + public abstract void invitationDeclined(String invitee, String reason); + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/MultiUserChat.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/MultiUserChat.java new file mode 100644 index 000000000..080084285 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/MultiUserChat.java @@ -0,0 +1,2668 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.filter.*; +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.NodeInformationProvider; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.*; + +import java.lang.ref.WeakReference; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; + +/** + * A MultiUserChat is a conversation that takes place among many users in a virtual + * room. A room could have many occupants with different affiliation and roles. + * Possible affiliatons are "owner", "admin", "member", and "outcast". Possible roles + * are "moderator", "participant", and "visitor". Each role and affiliation guarantees + * different privileges (e.g. Send messages to all occupants, Kick participants and visitors, + * Grant voice, Edit member list, etc.). + * + * @author Gaston Dombiak + */ +public class MultiUserChat { + + private final static String discoNamespace = "http://jabber.org/protocol/muc"; + private final static String discoNode = "http://jabber.org/protocol/muc#rooms"; + + private static Map joinedRooms = new WeakHashMap(); + + private XMPPConnection connection; + private String room; + private String subject; + private String nickname = null; + private boolean joined = false; + private Map occupantsMap = new HashMap(); + + private List invitationRejectionListeners = new ArrayList(); + private List subjectUpdatedListeners = new ArrayList(); + private List userStatusListeners = new ArrayList(); + private List participantStatusListeners = new ArrayList(); + + private PacketFilter presenceFilter; + private PacketListener presenceListener; + private List presenceInterceptors = new ArrayList(); + private PacketFilter subjectFilter; + private PacketListener subjectListener; + private PacketFilter messageFilter; + private PacketFilter declinesFilter; + private PacketListener declinesListener; + private PacketCollector messageCollector; + private List connectionListeners = new ArrayList(); + + static { + XMPPConnection.addConnectionListener(new ConnectionEstablishedListener() { + public void connectionEstablished(final XMPPConnection connection) { + // Set on every established connection that this client supports the Multi-User + // Chat protocol. This information will be used when another client tries to + // discover whether this client supports MUC or not. + ServiceDiscoveryManager.getInstanceFor(connection).addFeature(discoNamespace); + // Set the NodeInformationProvider that will provide information about the + // joined rooms whenever a disco request is received + ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider( + discoNode, + new NodeInformationProvider() { + public Iterator getNodeItems() { + ArrayList answer = new ArrayList(); + Iterator rooms=MultiUserChat.getJoinedRooms(connection); + while (rooms.hasNext()) { + answer.add(new DiscoverItems.Item((String)rooms.next())); + } + return answer.iterator(); + } + }); + } + }); + } + + /** + * Creates a new multi user chat with the specified connection and room name. Note: no + * information is sent to or received from the server until you attempt to + * {@link #join(String) join} the chat room. On some server implementations, + * the room will not be created until the first person joins it.<p> + * + * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com + * for the XMPP server example.com). You must ensure that the room address you're + * trying to connect to includes the proper chat sub-domain. + * + * @param connection the XMPP connection. + * @param room the name of the room in the form "roomName@service", where + * "service" is the hostname at which the multi-user chat + * service is running. Make sure to provide a valid JID. + */ + public MultiUserChat(XMPPConnection connection, String room) { + this.connection = connection; + this.room = room.toLowerCase(); + init(); + } + + /** + * Returns true if the specified user supports the Multi-User Chat protocol. + * + * @param connection the connection to use to perform the service discovery. + * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com. + * @return a boolean indicating whether the specified user supports the MUC protocol. + */ + public static boolean isServiceEnabled(XMPPConnection connection, String user) { + try { + DiscoverInfo result = + ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(user); + return result.containsFeature(discoNamespace); + } + catch (XMPPException e) { + e.printStackTrace(); + return false; + } + } + + /** + * Returns an Iterator on the rooms where the user has joined using a given connection. + * The Iterator will contain Strings where each String represents a room + * (e.g. room@muc.jabber.org). + * + * @param connection the connection used to join the rooms. + * @return an Iterator on the rooms where the user has joined using a given connection. + */ + private static Iterator getJoinedRooms(XMPPConnection connection) { + ArrayList rooms = (ArrayList)joinedRooms.get(connection); + if (rooms != null) { + return rooms.iterator(); + } + // Return an iterator on an empty collection (i.e. the user never joined a room) + return new ArrayList().iterator(); + } + + /** + * Returns an Iterator on the rooms where the requested user has joined. The Iterator will + * contain Strings where each String represents a room (e.g. room@muc.jabber.org). + * + * @param connection the connection to use to perform the service discovery. + * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com. + * @return an Iterator on the rooms where the requested user has joined. + */ + public static Iterator getJoinedRooms(XMPPConnection connection, String user) { + try { + ArrayList answer = new ArrayList(); + // Send the disco packet to the user + DiscoverItems result = + ServiceDiscoveryManager.getInstanceFor(connection).discoverItems(user, discoNode); + // Collect the entityID for each returned item + for (Iterator items=result.getItems(); items.hasNext();) { + answer.add(((DiscoverItems.Item)items.next()).getEntityID()); + } + return answer.iterator(); + } + catch (XMPPException e) { + e.printStackTrace(); + // Return an iterator on an empty collection + return new ArrayList().iterator(); + } + } + + /** + * Returns the discovered information of a given room without actually having to join the room. + * The server will provide information only for rooms that are public. + * + * @param connection the XMPP connection to use for discovering information about the room. + * @param room the name of the room in the form "roomName@service" of which we want to discover + * its information. + * @return the discovered information of a given room without actually having to join the room. + * @throws XMPPException if an error occured while trying to discover information of a room. + */ + public static RoomInfo getRoomInfo(XMPPConnection connection, String room) + throws XMPPException { + DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(room); + return new RoomInfo(info); + } + + /** + * Returns a collection with the XMPP addresses of the Multi-User Chat services. + * + * @param connection the XMPP connection to use for discovering Multi-User Chat services. + * @return a collection with the XMPP addresses of the Multi-User Chat services. + * @throws XMPPException if an error occured while trying to discover MUC services. + */ + public static Collection getServiceNames(XMPPConnection connection) throws XMPPException { + final List answer = new ArrayList(); + ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection); + DiscoverItems items = discoManager.discoverItems(connection.getServiceName()); + for (Iterator it = items.getItems(); it.hasNext();) { + DiscoverItems.Item item = (DiscoverItems.Item) it.next(); + try { + DiscoverInfo info = discoManager.discoverInfo(item.getEntityID()); + if (info.containsFeature("http://jabber.org/protocol/muc")) { + answer.add(item.getEntityID()); + } + } + catch (XMPPException e) { + // Trouble finding info in some cases. This is a workaround for + // discovering info on remote servers. + } + } + return answer; + } + + /** + * Returns a collection of HostedRooms where each HostedRoom has the XMPP address of the room + * and the room's name. Once discovered the rooms hosted by a chat service it is possible to + * discover more detailed room information or join the room. + * + * @param connection the XMPP connection to use for discovering hosted rooms by the MUC service. + * @param serviceName the service that is hosting the rooms to discover. + * @return a collection of HostedRooms. + * @throws XMPPException if an error occured while trying to discover the information. + */ + public static Collection getHostedRooms(XMPPConnection connection, String serviceName) + throws XMPPException { + List answer = new ArrayList(); + ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection); + DiscoverItems items = discoManager.discoverItems(serviceName); + for (Iterator it = items.getItems(); it.hasNext();) { + DiscoverItems.Item item = (DiscoverItems.Item) it.next(); + answer.add(new HostedRoom(item)); + } + return answer; + } + + /** + * Returns the name of the room this MultiUserChat object represents. + * + * @return the multi user chat room name. + */ + public String getRoom() { + return room; + } + + /** + * Creates the room according to some default configuration, assign the requesting user + * as the room owner, and add the owner to the room but not allow anyone else to enter + * the room (effectively "locking" the room). The requesting user will join the room + * under the specified nickname as soon as the room has been created.<p> + * + * To create an "Instant Room", that means a room with some default configuration that is + * available for immediate access, the room's owner should send an empty form after creating + * the room. {@link #sendConfigurationForm(Form)}<p> + * + * To create a "Reserved Room", that means a room manually configured by the room creator + * before anyone is allowed to enter, the room's owner should complete and send a form after + * creating the room. Once the completed configutation form is sent to the server, the server + * will unlock the room. {@link #sendConfigurationForm(Form)} + * + * @param nickname the nickname to use. + * @throws XMPPException if the room couldn't be created for some reason + * (e.g. room already exists; user already joined to an existant room or + * 405 error if the user is not allowed to create the room) + */ + public synchronized void create(String nickname) throws XMPPException { + if (nickname == null || nickname.equals("")) { + throw new IllegalArgumentException("Nickname must not be null or blank."); + } + // If we've already joined the room, leave it before joining under a new + // nickname. + if (joined) { + throw new IllegalStateException("Creation failed - User already joined the room."); + } + // We create a room by sending a presence packet to room@service/nick + // and signal support for MUC. The owner will be automatically logged into the room. + Presence joinPresence = new Presence(Presence.Type.AVAILABLE); + joinPresence.setTo(room + "/" + nickname); + // Indicate the the client supports MUC + joinPresence.addExtension(new MUCInitialPresence()); + // Invoke presence interceptors so that extra information can be dynamically added + for (Iterator it = presenceInterceptors.iterator(); it.hasNext();) { + PacketInterceptor packetInterceptor = (PacketInterceptor) it.next(); + packetInterceptor.interceptPacket(joinPresence); + } + + // Wait for a presence packet back from the server. + PacketFilter responseFilter = + new AndFilter( + new FromMatchesFilter(room + "/" + nickname), + new PacketTypeFilter(Presence.class)); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send create & join packet. + connection.sendPacket(joinPresence); + // Wait up to a certain number of seconds for a reply. + Presence presence = + (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (presence == null) { + throw new XMPPException("No response from server."); + } + else if (presence.getError() != null) { + throw new XMPPException(presence.getError()); + } + // Whether the room existed before or was created, the user has joined the room + this.nickname = nickname; + joined = true; + userHasJoined(); + + // Look for confirmation of room creation from the server + MUCUser mucUser = getMUCUserExtension(presence); + if (mucUser != null && mucUser.getStatus() != null) { + if ("201".equals(mucUser.getStatus().getCode())) { + // Room was created and the user has joined the room + return; + } + } + // We need to leave the room since it seems that the room already existed + leave(); + throw new XMPPException("Creation failed - Missing acknowledge of room creation."); + } + + /** + * Joins the chat room using the specified nickname. If already joined + * using another nickname, this method will first leave the room and then + * re-join using the new nickname. The default timeout of Smack for a reply + * from the group chat server that the join succeeded will be used. After + * joining the room, the room will decide the amount of history to send. + * + * @param nickname the nickname to use. + * @throws XMPPException if an error occurs joining the room. In particular, a + * 401 error can occur if no password was provided and one is required; or a + * 403 error can occur if the user is banned; or a + * 404 error can occur if the room does not exist or is locked; or a + * 407 error can occur if user is not on the member list; or a + * 409 error can occur if someone is already in the group chat with the same nickname. + */ + public void join(String nickname) throws XMPPException { + join(nickname, null, null, SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Joins the chat room using the specified nickname and password. If already joined + * using another nickname, this method will first leave the room and then + * re-join using the new nickname. The default timeout of Smack for a reply + * from the group chat server that the join succeeded will be used. After + * joining the room, the room will decide the amount of history to send.<p> + * + * A password is required when joining password protected rooms. If the room does + * not require a password there is no need to provide one. + * + * @param nickname the nickname to use. + * @param password the password to use. + * @throws XMPPException if an error occurs joining the room. In particular, a + * 401 error can occur if no password was provided and one is required; or a + * 403 error can occur if the user is banned; or a + * 404 error can occur if the room does not exist or is locked; or a + * 407 error can occur if user is not on the member list; or a + * 409 error can occur if someone is already in the group chat with the same nickname. + */ + public void join(String nickname, String password) throws XMPPException { + join(nickname, password, null, SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Joins the chat room using the specified nickname and password. If already joined + * using another nickname, this method will first leave the room and then + * re-join using the new nickname.<p> + * + * To control the amount of history to receive while joining a room you will need to provide + * a configured DiscussionHistory object.<p> + * + * A password is required when joining password protected rooms. If the room does + * not require a password there is no need to provide one.<p> + * + * If the room does not already exist when the user seeks to enter it, the server will + * decide to create a new room or not. + * + * @param nickname the nickname to use. + * @param password the password to use. + * @param history the amount of discussion history to receive while joining a room. + * @param timeout the amount of time to wait for a reply from the MUC service(in milleseconds). + * @throws XMPPException if an error occurs joining the room. In particular, a + * 401 error can occur if no password was provided and one is required; or a + * 403 error can occur if the user is banned; or a + * 404 error can occur if the room does not exist or is locked; or a + * 407 error can occur if user is not on the member list; or a + * 409 error can occur if someone is already in the group chat with the same nickname. + */ + public synchronized void join( + String nickname, + String password, + DiscussionHistory history, + long timeout) + throws XMPPException { + if (nickname == null || nickname.equals("")) { + throw new IllegalArgumentException("Nickname must not be null or blank."); + } + // If we've already joined the room, leave it before joining under a new + // nickname. + if (joined) { + leave(); + } + // We join a room by sending a presence packet where the "to" + // field is in the form "roomName@service/nickname" + Presence joinPresence = new Presence(Presence.Type.AVAILABLE); + joinPresence.setTo(room + "/" + nickname); + + // Indicate the the client supports MUC + MUCInitialPresence mucInitialPresence = new MUCInitialPresence(); + if (password != null) { + mucInitialPresence.setPassword(password); + } + if (history != null) { + mucInitialPresence.setHistory(history.getMUCHistory()); + } + joinPresence.addExtension(mucInitialPresence); + // Invoke presence interceptors so that extra information can be dynamically added + for (Iterator it = presenceInterceptors.iterator(); it.hasNext();) { + PacketInterceptor packetInterceptor = (PacketInterceptor) it.next(); + packetInterceptor.interceptPacket(joinPresence); + } + + // Wait for a presence packet back from the server. + PacketFilter responseFilter = + new AndFilter( + new FromMatchesFilter(room + "/" + nickname), + new PacketTypeFilter(Presence.class)); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send join packet. + connection.sendPacket(joinPresence); + // Wait up to a certain number of seconds for a reply. + Presence presence = (Presence) response.nextResult(timeout); + // Stop queuing results + response.cancel(); + + if (presence == null) { + throw new XMPPException("No response from server."); + } + else if (presence.getError() != null) { + throw new XMPPException(presence.getError()); + } + this.nickname = nickname; + joined = true; + userHasJoined(); + } + + /** + * Returns true if currently in the multi user chat (after calling the {@link + * #join(String)} method). + * + * @return true if currently in the multi user chat room. + */ + public boolean isJoined() { + return joined; + } + + /** + * Leave the chat room. + */ + public synchronized void leave() { + // If not joined already, do nothing. + if (!joined) { + return; + } + // We leave a room by sending a presence packet where the "to" + // field is in the form "roomName@service/nickname" + Presence leavePresence = new Presence(Presence.Type.UNAVAILABLE); + leavePresence.setTo(room + "/" + nickname); + // Invoke presence interceptors so that extra information can be dynamically added + for (Iterator it = presenceInterceptors.iterator(); it.hasNext();) { + PacketInterceptor packetInterceptor = (PacketInterceptor) it.next(); + packetInterceptor.interceptPacket(leavePresence); + } + connection.sendPacket(leavePresence); + // Reset occupant information. + occupantsMap = new HashMap(); + nickname = null; + joined = false; + userHasLeft(); + } + + /** + * Returns the room's configuration form that the room's owner can use or <tt>null</tt> if + * no configuration is possible. The configuration form allows to set the room's language, + * enable logging, specify room's type, etc.. + * + * @return the Form that contains the fields to complete together with the instrucions or + * <tt>null</tt> if no configuration is possible. + * @throws XMPPException if an error occurs asking the configuration form for the room. + */ + public Form getConfigurationForm() throws XMPPException { + MUCOwner iq = new MUCOwner(); + iq.setTo(room); + iq.setType(IQ.Type.GET); + + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Request the configuration form to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + return Form.getFormFrom(answer); + } + + /** + * Sends the completed configuration form to the server. The room will be configured + * with the new settings defined in the form. If the form is empty then the server + * will create an instant room (will use default configuration). + * + * @param form the form with the new settings. + * @throws XMPPException if an error occurs setting the new rooms' configuration. + */ + public void sendConfigurationForm(Form form) throws XMPPException { + MUCOwner iq = new MUCOwner(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + iq.addExtension(form.getDataFormToSend()); + + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the completed configuration form to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + /** + * Returns the room's registration form that an unaffiliated user, can use to become a member + * of the room or <tt>null</tt> if no registration is possible. Some rooms may restrict the + * privilege to register members and allow only room admins to add new members.<p> + * + * If the user requesting registration requirements is not allowed to register with the room + * (e.g. because that privilege has been restricted), the room will return a "Not Allowed" + * error to the user (error code 405). + * + * @return the registration Form that contains the fields to complete together with the + * instrucions or <tt>null</tt> if no registration is possible. + * @throws XMPPException if an error occurs asking the registration form for the room or a + * 405 error if the user is not allowed to register with the room. + */ + public Form getRegistrationForm() throws XMPPException { + Registration reg = new Registration(); + reg.setType(IQ.Type.GET); + reg.setTo(room); + + PacketFilter filter = + new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class)); + PacketCollector collector = connection.createPacketCollector(filter); + connection.sendPacket(reg); + IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from server."); + } + else if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + return Form.getFormFrom(result); + } + + /** + * Sends the completed registration form to the server. After the user successfully submits + * the form, the room may queue the request for review by the room admins or may immediately + * add the user to the member list by changing the user's affiliation from "none" to "member.<p> + * + * If the desired room nickname is already reserved for that room, the room will return a + * "Conflict" error to the user (error code 409). If the room does not support registration, + * it will return a "Service Unavailable" error to the user (error code 503). + * + * @param form the completed registration form. + * @throws XMPPException if an error occurs submitting the registration form. In particular, a + * 409 error can occur if the desired room nickname is already reserved for that room; + * or a 503 error can occur if the room does not support registration. + */ + public void sendRegistrationForm(Form form) throws XMPPException { + Registration reg = new Registration(); + reg.setType(IQ.Type.SET); + reg.setTo(room); + reg.addExtension(form.getDataFormToSend()); + + PacketFilter filter = + new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class)); + PacketCollector collector = connection.createPacketCollector(filter); + connection.sendPacket(reg); + IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from server."); + } + else if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + } + + /** + * Sends a request to the server to destroy the room. The sender of the request + * should be the room's owner. If the sender of the destroy request is not the room's owner + * then the server will answer a "Forbidden" error (403). + * + * @param reason the reason for the room destruction. + * @param alternateJID the JID of an alternate location. + * @throws XMPPException if an error occurs while trying to destroy the room. + * An error can occur which will be wrapped by an XMPPException -- + * XMPP error code 403. The error code can be used to present more + * appropiate error messages to end-users. + */ + public void destroy(String reason, String alternateJID) throws XMPPException { + MUCOwner iq = new MUCOwner(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + + // Create the reason for the room destruction + MUCOwner.Destroy destroy = new MUCOwner.Destroy(); + destroy.setReason(reason); + destroy.setJid(alternateJID); + iq.setDestroy(destroy); + + // Wait for a presence packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the room destruction request. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + // Reset occupant information. + occupantsMap = new HashMap(); + nickname = null; + joined = false; + userHasLeft(); + } + + /** + * Invites another user to the room in which one is an occupant. The invitation + * will be sent to the room which in turn will forward the invitation to the invitee.<p> + * + * If the room is password-protected, the invitee will receive a password to use to join + * the room. If the room is members-only, the the invitee may be added to the member list. + * + * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit) + * @param reason the reason why the user is being invited. + */ + public void invite(String user, String reason) { + invite(new Message(), user, reason); + } + + /** + * Invites another user to the room in which one is an occupant using a given Message. The invitation + * will be sent to the room which in turn will forward the invitation to the invitee.<p> + * + * If the room is password-protected, the invitee will receive a password to use to join + * the room. If the room is members-only, the the invitee may be added to the member list. + * + * @param message the message to use for sending the invitation. + * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit) + * @param reason the reason why the user is being invited. + */ + public void invite(Message message, String user, String reason) { + // TODO listen for 404 error code when inviter supplies a non-existent JID + message.setTo(room); + + // Create the MUCUser packet that will include the invitation + MUCUser mucUser = new MUCUser(); + MUCUser.Invite invite = new MUCUser.Invite(); + invite.setTo(user); + invite.setReason(reason); + mucUser.setInvite(invite); + // Add the MUCUser packet that includes the invitation to the message + message.addExtension(mucUser); + + connection.sendPacket(message); + } + + /** + * Informs the sender of an invitation that the invitee declines the invitation. The rejection + * will be sent to the room which in turn will forward the rejection to the inviter. + * + * @param conn the connection to use for sending the rejection. + * @param room the room that sent the original invitation. + * @param inviter the inviter of the declined invitation. + * @param reason the reason why the invitee is declining the invitation. + */ + public static void decline(XMPPConnection conn, String room, String inviter, String reason) { + Message message = new Message(room); + + // Create the MUCUser packet that will include the rejection + MUCUser mucUser = new MUCUser(); + MUCUser.Decline decline = new MUCUser.Decline(); + decline.setTo(inviter); + decline.setReason(reason); + mucUser.setDecline(decline); + // Add the MUCUser packet that includes the rejection + message.addExtension(mucUser); + + conn.sendPacket(message); + } + + /** + * Adds a listener to invitation notifications. The listener will be fired anytime + * an invitation is received. + * + * @param conn the connection where the listener will be applied. + * @param listener an invitation listener. + */ + public static void addInvitationListener(XMPPConnection conn, InvitationListener listener) { + InvitationsMonitor.getInvitationsMonitor(conn).addInvitationListener(listener); + } + + /** + * Removes a listener to invitation notifications. The listener will be fired anytime + * an invitation is received. + * + * @param conn the connection where the listener was applied. + * @param listener an invitation listener. + */ + public static void removeInvitationListener(XMPPConnection conn, InvitationListener listener) { + InvitationsMonitor.getInvitationsMonitor(conn).removeInvitationListener(listener); + } + + /** + * Adds a listener to invitation rejections notifications. The listener will be fired anytime + * an invitation is declined. + * + * @param listener an invitation rejection listener. + */ + public void addInvitationRejectionListener(InvitationRejectionListener listener) { + synchronized (invitationRejectionListeners) { + if (!invitationRejectionListeners.contains(listener)) { + invitationRejectionListeners.add(listener); + } + } + } + + /** + * Removes a listener from invitation rejections notifications. The listener will be fired + * anytime an invitation is declined. + * + * @param listener an invitation rejection listener. + */ + public void removeInvitationRejectionListener(InvitationRejectionListener listener) { + synchronized (invitationRejectionListeners) { + invitationRejectionListeners.remove(listener); + } + } + + /** + * Fires invitation rejection listeners. + */ + private void fireInvitationRejectionListeners(String invitee, String reason) { + InvitationRejectionListener[] listeners = null; + synchronized (invitationRejectionListeners) { + listeners = new InvitationRejectionListener[invitationRejectionListeners.size()]; + invitationRejectionListeners.toArray(listeners); + } + for (int i = 0; i < listeners.length; i++) { + listeners[i].invitationDeclined(invitee, reason); + } + } + + /** + * Adds a listener to subject change notifications. The listener will be fired anytime + * the room's subject changes. + * + * @param listener a subject updated listener. + */ + public void addSubjectUpdatedListener(SubjectUpdatedListener listener) { + synchronized (subjectUpdatedListeners) { + if (!subjectUpdatedListeners.contains(listener)) { + subjectUpdatedListeners.add(listener); + } + } + } + + /** + * Removes a listener from subject change notifications. The listener will be fired + * anytime the room's subject changes. + * + * @param listener a subject updated listener. + */ + public void removeSubjectUpdatedListener(SubjectUpdatedListener listener) { + synchronized (subjectUpdatedListeners) { + subjectUpdatedListeners.remove(listener); + } + } + + /** + * Fires subject updated listeners. + */ + private void fireSubjectUpdatedListeners(String subject, String from) { + SubjectUpdatedListener[] listeners = null; + synchronized (subjectUpdatedListeners) { + listeners = new SubjectUpdatedListener[subjectUpdatedListeners.size()]; + subjectUpdatedListeners.toArray(listeners); + } + for (int i = 0; i < listeners.length; i++) { + listeners[i].subjectUpdated(subject, from); + } + } + + /** + * Adds a new {@link PacketInterceptor} that will be invoked every time a new presence + * is going to be sent by this MultiUserChat to the server. Packet interceptors may + * add new extensions to the presence that is going to be sent to the MUC service. + * + * @param presenceInterceptor the new packet interceptor that will intercept presence packets. + */ + public void addPresenceInterceptor(PacketInterceptor presenceInterceptor) { + presenceInterceptors.add(presenceInterceptor); + } + + /** + * Removes a {@link PacketInterceptor} that was being invoked every time a new presence + * was being sent by this MultiUserChat to the server. Packet interceptors may + * add new extensions to the presence that is going to be sent to the MUC service. + * + * @param presenceInterceptor the packet interceptor to remove. + */ + public void removePresenceInterceptor(PacketInterceptor presenceInterceptor) { + presenceInterceptors.remove(presenceInterceptor); + } + + /** + * Returns the last known room's subject or <tt>null</tt> if the user hasn't joined the room + * or the room does not have a subject yet. In case the room has a subject, as soon as the + * user joins the room a message with the current room's subject will be received.<p> + * + * To be notified every time the room's subject change you should add a listener + * to this room. {@link #addSubjectUpdatedListener(SubjectUpdatedListener)}<p> + * + * To change the room's subject use {@link #changeSubject(String)}. + * + * @return the room's subject or <tt>null</tt> if the user hasn't joined the room or the + * room does not have a subject yet. + */ + public String getSubject() { + return subject; + } + + /** + * Returns the reserved room nickname for the user in the room. A user may have a reserved + * nickname, for example through explicit room registration or database integration. In such + * cases it may be desirable for the user to discover the reserved nickname before attempting + * to enter the room. + * + * @return the reserved room nickname or <tt>null</tt> if none. + */ + public String getReservedNickname() { + try { + DiscoverInfo result = + ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo( + room, + "x-roomuser-item"); + // Look for an Identity that holds the reserved nickname and return its name + for (Iterator identities = result.getIdentities(); identities.hasNext();) { + DiscoverInfo.Identity identity = (DiscoverInfo.Identity) identities.next(); + return identity.getName(); + } + // If no Identity was found then the user does not have a reserved room nickname + return null; + } + catch (XMPPException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Returns the nickname that was used to join the room, or <tt>null</tt> if not + * currently joined. + * + * @return the nickname currently being used. + */ + public String getNickname() { + return nickname; + } + + /** + * Changes the occupant's nickname to a new nickname within the room. Each room occupant + * will receive two presence packets. One of type "unavailable" for the old nickname and one + * indicating availability for the new nickname. The unavailable presence will contain the new + * nickname and an appropriate status code (namely 303) as extended presence information. The + * status code 303 indicates that the occupant is changing his/her nickname. + * + * @param nickname the new nickname within the room. + * @throws XMPPException if the new nickname is already in use by another occupant. + */ + public void changeNickname(String nickname) throws XMPPException { + if (nickname == null || nickname.equals("")) { + throw new IllegalArgumentException("Nickname must not be null or blank."); + } + // Check that we already have joined the room before attempting to change the + // nickname. + if (!joined) { + throw new IllegalStateException("Must be logged into the room to change nickname."); + } + // We change the nickname by sending a presence packet where the "to" + // field is in the form "roomName@service/nickname" + // We don't have to signal the MUC support again + Presence joinPresence = new Presence(Presence.Type.AVAILABLE); + joinPresence.setTo(room + "/" + nickname); + // Invoke presence interceptors so that extra information can be dynamically added + for (Iterator it = presenceInterceptors.iterator(); it.hasNext();) { + PacketInterceptor packetInterceptor = (PacketInterceptor) it.next(); + packetInterceptor.interceptPacket(joinPresence); + } + + // Wait for a presence packet back from the server. + PacketFilter responseFilter = + new AndFilter( + new FromMatchesFilter(room + "/" + nickname), + new PacketTypeFilter(Presence.class)); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send join packet. + connection.sendPacket(joinPresence); + // Wait up to a certain number of seconds for a reply. + Presence presence = + (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (presence == null) { + throw new XMPPException("No response from server."); + } + else if (presence.getError() != null) { + throw new XMPPException(presence.getError()); + } + this.nickname = nickname; + } + + /** + * Changes the occupant's availability status within the room. The presence type + * will remain available but with a new status that describes the presence update and + * a new presence mode (e.g. Extended away). + * + * @param status a text message describing the presence update. + * @param mode the mode type for the presence update. + */ + public void changeAvailabilityStatus(String status, Presence.Mode mode) { + if (nickname == null || nickname.equals("")) { + throw new IllegalArgumentException("Nickname must not be null or blank."); + } + // Check that we already have joined the room before attempting to change the + // availability status. + if (!joined) { + throw new IllegalStateException( + "Must be logged into the room to change the " + "availability status."); + } + // We change the availability status by sending a presence packet to the room with the + // new presence status and mode + Presence joinPresence = new Presence(Presence.Type.AVAILABLE); + joinPresence.setStatus(status); + joinPresence.setMode(mode); + joinPresence.setTo(room + "/" + nickname); + // Invoke presence interceptors so that extra information can be dynamically added + for (Iterator it = presenceInterceptors.iterator(); it.hasNext();) { + PacketInterceptor packetInterceptor = (PacketInterceptor) it.next(); + packetInterceptor.interceptPacket(joinPresence); + } + + // Send join packet. + connection.sendPacket(joinPresence); + } + + /** + * Kicks a visitor or participant from the room. The kicked occupant will receive a presence + * of type "unavailable" including a status code 307 and optionally along with the reason + * (if provided) and the bare JID of the user who initiated the kick. After the occupant + * was kicked from the room, the rest of the occupants will receive a presence of type + * "unavailable". The presence will include a status code 307 which means that the occupant + * was kicked from the room. + * + * @param nickname the nickname of the participant or visitor to kick from the room + * (e.g. "john"). + * @param reason the reason why the participant or visitor is being kicked from the room. + * @throws XMPPException if an error occurs kicking the occupant. In particular, a + * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin" + * was intended to be kicked (i.e. Not Allowed error); or a + * 403 error can occur if the occupant that intended to kick another occupant does + * not have kicking privileges (i.e. Forbidden error); or a + * 400 error can occur if the provided nickname is not present in the room. + */ + public void kickParticipant(String nickname, String reason) throws XMPPException { + changeRole(nickname, "none", reason); + } + + /** + * Grants voice to visitors in the room. In a moderated room, a moderator may want to manage + * who does and does not have "voice" in the room. To have voice means that a room occupant + * is able to send messages to the room occupants. + * + * @param nicknames the nicknames of the visitors to grant voice in the room (e.g. "john"). + * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a + * 403 error can occur if the occupant that intended to grant voice is not + * a moderator in this room (i.e. Forbidden error); or a + * 400 error can occur if the provided nickname is not present in the room. + */ + public void grantVoice(Collection nicknames) throws XMPPException { + changeRole(nicknames, "participant"); + } + + /** + * Grants voice to a visitor in the room. In a moderated room, a moderator may want to manage + * who does and does not have "voice" in the room. To have voice means that a room occupant + * is able to send messages to the room occupants. + * + * @param nickname the nickname of the visitor to grant voice in the room (e.g. "john"). + * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a + * 403 error can occur if the occupant that intended to grant voice is not + * a moderator in this room (i.e. Forbidden error); or a + * 400 error can occur if the provided nickname is not present in the room. + */ + public void grantVoice(String nickname) throws XMPPException { + changeRole(nickname, "participant", null); + } + + /** + * Revokes voice from participants in the room. In a moderated room, a moderator may want to + * revoke an occupant's privileges to speak. To have voice means that a room occupant + * is able to send messages to the room occupants. + * + * @param nicknames the nicknames of the participants to revoke voice (e.g. "john"). + * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a + * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin" + * was tried to revoke his voice (i.e. Not Allowed error); or a + * 400 error can occur if the provided nickname is not present in the room. + */ + public void revokeVoice(Collection nicknames) throws XMPPException { + changeRole(nicknames, "visitor"); + } + + /** + * Revokes voice from a participant in the room. In a moderated room, a moderator may want to + * revoke an occupant's privileges to speak. To have voice means that a room occupant + * is able to send messages to the room occupants. + * + * @param nickname the nickname of the participant to revoke voice (e.g. "john"). + * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a + * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin" + * was tried to revoke his voice (i.e. Not Allowed error); or a + * 400 error can occur if the provided nickname is not present in the room. + */ + public void revokeVoice(String nickname) throws XMPPException { + changeRole(nickname, "visitor", null); + } + + /** + * Bans users from the room. An admin or owner of the room can ban users from a room. This + * means that the banned user will no longer be able to join the room unless the ban has been + * removed. If the banned user was present in the room then he/she will be removed from the + * room and notified that he/she was banned along with the reason (if provided) and the bare + * XMPP user ID of the user who initiated the ban. + * + * @param jids the bare XMPP user IDs of the users to ban. + * @throws XMPPException if an error occurs banning a user. In particular, a + * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin" + * was tried to be banned (i.e. Not Allowed error). + */ + public void banUsers(Collection jids) throws XMPPException { + changeAffiliationByAdmin(jids, "outcast"); + } + + /** + * Bans a user from the room. An admin or owner of the room can ban users from a room. This + * means that the banned user will no longer be able to join the room unless the ban has been + * removed. If the banned user was present in the room then he/she will be removed from the + * room and notified that he/she was banned along with the reason (if provided) and the bare + * XMPP user ID of the user who initiated the ban. + * + * @param jid the bare XMPP user ID of the user to ban (e.g. "user@host.org"). + * @param reason the reason why the user was banned. + * @throws XMPPException if an error occurs banning a user. In particular, a + * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin" + * was tried to be banned (i.e. Not Allowed error). + */ + public void banUser(String jid, String reason) throws XMPPException { + changeAffiliationByAdmin(jid, "outcast", reason); + } + + /** + * Grants membership to other users. Only administrators are able to grant membership. A user + * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room + * that a user cannot enter without being on the member list). + * + * @param jids the XMPP user IDs of the users to grant membership. + * @throws XMPPException if an error occurs granting membership to a user. + */ + public void grantMembership(Collection jids) throws XMPPException { + changeAffiliationByAdmin(jids, "member"); + } + + /** + * Grants membership to a user. Only administrators are able to grant membership. A user + * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room + * that a user cannot enter without being on the member list). + * + * @param jid the XMPP user ID of the user to grant membership (e.g. "user@host.org"). + * @throws XMPPException if an error occurs granting membership to a user. + */ + public void grantMembership(String jid) throws XMPPException { + changeAffiliationByAdmin(jid, "member", null); + } + + /** + * Revokes users' membership. Only administrators are able to revoke membership. A user + * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room + * that a user cannot enter without being on the member list). If the user is in the room and + * the room is of type members-only then the user will be removed from the room. + * + * @param jids the bare XMPP user IDs of the users to revoke membership. + * @throws XMPPException if an error occurs revoking membership to a user. + */ + public void revokeMembership(Collection jids) throws XMPPException { + changeAffiliationByAdmin(jids, "none"); + } + + /** + * Revokes a user's membership. Only administrators are able to revoke membership. A user + * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room + * that a user cannot enter without being on the member list). If the user is in the room and + * the room is of type members-only then the user will be removed from the room. + * + * @param jid the bare XMPP user ID of the user to revoke membership (e.g. "user@host.org"). + * @throws XMPPException if an error occurs revoking membership to a user. + */ + public void revokeMembership(String jid) throws XMPPException { + changeAffiliationByAdmin(jid, "none", null); + } + + /** + * Grants moderator privileges to participants or visitors. Room administrators may grant + * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite + * other users, modify room's subject plus all the partcipants privileges. + * + * @param nicknames the nicknames of the occupants to grant moderator privileges. + * @throws XMPPException if an error occurs granting moderator privileges to a user. + */ + public void grantModerator(Collection nicknames) throws XMPPException { + changeRole(nicknames, "moderator"); + } + + /** + * Grants moderator privileges to a participant or visitor. Room administrators may grant + * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite + * other users, modify room's subject plus all the partcipants privileges. + * + * @param nickname the nickname of the occupant to grant moderator privileges. + * @throws XMPPException if an error occurs granting moderator privileges to a user. + */ + public void grantModerator(String nickname) throws XMPPException { + changeRole(nickname, "moderator", null); + } + + /** + * Revokes moderator privileges from other users. The occupant that loses moderator + * privileges will become a participant. Room administrators may revoke moderator privileges + * only to occupants whose affiliation is member or none. This means that an administrator is + * not allowed to revoke moderator privileges from other room administrators or owners. + * + * @param nicknames the nicknames of the occupants to revoke moderator privileges. + * @throws XMPPException if an error occurs revoking moderator privileges from a user. + */ + public void revokeModerator(Collection nicknames) throws XMPPException { + changeRole(nicknames, "participant"); + } + + /** + * Revokes moderator privileges from another user. The occupant that loses moderator + * privileges will become a participant. Room administrators may revoke moderator privileges + * only to occupants whose affiliation is member or none. This means that an administrator is + * not allowed to revoke moderator privileges from other room administrators or owners. + * + * @param nickname the nickname of the occupant to revoke moderator privileges. + * @throws XMPPException if an error occurs revoking moderator privileges from a user. + */ + public void revokeModerator(String nickname) throws XMPPException { + changeRole(nickname, "participant", null); + } + + /** + * Grants ownership privileges to other users. Room owners may grant ownership privileges. + * Some room implementations will not allow to grant ownership privileges to other users. + * An owner is allowed to change defining room features as well as perform all administrative + * functions. + * + * @param jids the collection of bare XMPP user IDs of the users to grant ownership. + * @throws XMPPException if an error occurs granting ownership privileges to a user. + */ + public void grantOwnership(Collection jids) throws XMPPException { + changeAffiliationByOwner(jids, "owner"); + } + + /** + * Grants ownership privileges to another user. Room owners may grant ownership privileges. + * Some room implementations will not allow to grant ownership privileges to other users. + * An owner is allowed to change defining room features as well as perform all administrative + * functions. + * + * @param jid the bare XMPP user ID of the user to grant ownership (e.g. "user@host.org"). + * @throws XMPPException if an error occurs granting ownership privileges to a user. + */ + public void grantOwnership(String jid) throws XMPPException { + changeAffiliationByOwner(jid, "owner"); + } + + /** + * Revokes ownership privileges from other users. The occupant that loses ownership + * privileges will become an administrator. Room owners may revoke ownership privileges. + * Some room implementations will not allow to grant ownership privileges to other users. + * + * @param jids the bare XMPP user IDs of the users to revoke ownership. + * @throws XMPPException if an error occurs revoking ownership privileges from a user. + */ + public void revokeOwnership(Collection jids) throws XMPPException { + changeAffiliationByOwner(jids, "admin"); + } + + /** + * Revokes ownership privileges from another user. The occupant that loses ownership + * privileges will become an administrator. Room owners may revoke ownership privileges. + * Some room implementations will not allow to grant ownership privileges to other users. + * + * @param jid the bare XMPP user ID of the user to revoke ownership (e.g. "user@host.org"). + * @throws XMPPException if an error occurs revoking ownership privileges from a user. + */ + public void revokeOwnership(String jid) throws XMPPException { + changeAffiliationByOwner(jid, "admin"); + } + + /** + * Grants administrator privileges to other users. Room owners may grant administrator + * privileges to a member or unaffiliated user. An administrator is allowed to perform + * administrative functions such as banning users and edit moderator list. + * + * @param jids the bare XMPP user IDs of the users to grant administrator privileges. + * @throws XMPPException if an error occurs granting administrator privileges to a user. + */ + public void grantAdmin(Collection jids) throws XMPPException { + changeAffiliationByOwner(jids, "admin"); + } + + /** + * Grants administrator privileges to another user. Room owners may grant administrator + * privileges to a member or unaffiliated user. An administrator is allowed to perform + * administrative functions such as banning users and edit moderator list. + * + * @param jid the bare XMPP user ID of the user to grant administrator privileges + * (e.g. "user@host.org"). + * @throws XMPPException if an error occurs granting administrator privileges to a user. + */ + public void grantAdmin(String jid) throws XMPPException { + changeAffiliationByOwner(jid, "admin"); + } + + /** + * Revokes administrator privileges from users. The occupant that loses administrator + * privileges will become a member. Room owners may revoke administrator privileges from + * a member or unaffiliated user. + * + * @param jids the bare XMPP user IDs of the user to revoke administrator privileges. + * @throws XMPPException if an error occurs revoking administrator privileges from a user. + */ + public void revokeAdmin(Collection jids) throws XMPPException { + changeAffiliationByOwner(jids, "member"); + } + + /** + * Revokes administrator privileges from a user. The occupant that loses administrator + * privileges will become a member. Room owners may revoke administrator privileges from + * a member or unaffiliated user. + * + * @param jid the bare XMPP user ID of the user to revoke administrator privileges + * (e.g. "user@host.org"). + * @throws XMPPException if an error occurs revoking administrator privileges from a user. + */ + public void revokeAdmin(String jid) throws XMPPException { + changeAffiliationByOwner(jid, "member"); + } + + private void changeAffiliationByOwner(String jid, String affiliation) throws XMPPException { + MUCOwner iq = new MUCOwner(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + // Set the new affiliation. + MUCOwner.Item item = new MUCOwner.Item(affiliation); + item.setJid(jid); + iq.addItem(item); + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the change request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + private void changeAffiliationByOwner(Collection jids, String affiliation) + throws XMPPException { + MUCOwner iq = new MUCOwner(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + for (Iterator it=jids.iterator(); it.hasNext();) { + // Set the new affiliation. + MUCOwner.Item item = new MUCOwner.Item(affiliation); + item.setJid((String) it.next()); + iq.addItem(item); + } + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the change request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + private void changeAffiliationByAdmin(String jid, String affiliation, String reason) + throws XMPPException { + MUCAdmin iq = new MUCAdmin(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + // Set the new affiliation. + MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null); + item.setJid(jid); + item.setReason(reason); + iq.addItem(item); + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the change request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + private void changeAffiliationByAdmin(Collection jids, String affiliation) + throws XMPPException { + MUCAdmin iq = new MUCAdmin(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + for (Iterator it=jids.iterator(); it.hasNext();) { + // Set the new affiliation. + MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null); + item.setJid((String) it.next()); + iq.addItem(item); + } + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the change request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + private void changeRole(String nickname, String role, String reason) throws XMPPException { + MUCAdmin iq = new MUCAdmin(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + // Set the new role. + MUCAdmin.Item item = new MUCAdmin.Item(null, role); + item.setNick(nickname); + item.setReason(reason); + iq.addItem(item); + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the change request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + private void changeRole(Collection nicknames, String role) throws XMPPException { + MUCAdmin iq = new MUCAdmin(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + for (Iterator it=nicknames.iterator(); it.hasNext();) { + // Set the new role. + MUCAdmin.Item item = new MUCAdmin.Item(null, role); + item.setNick((String) it.next()); + iq.addItem(item); + } + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the change request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + /** + * Returns the number of occupants in the group chat.<p> + * + * Note: this value will only be accurate after joining the group chat, and + * may fluctuate over time. If you query this value directly after joining the + * group chat it may not be accurate, as it takes a certain amount of time for + * the server to send all presence packets to this client. + * + * @return the number of occupants in the group chat. + */ + public int getOccupantsCount() { + synchronized (occupantsMap) { + return occupantsMap.size(); + } + } + + /** + * Returns an Iterator (of Strings) for the list of fully qualified occupants + * in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser". + * Typically, a client would only display the nickname of the occupant. To + * get the nickname from the fully qualified name, use the + * {@link org.jivesoftware.smack.util.StringUtils#parseResource(String)} method. + * Note: this value will only be accurate after joining the group chat, and may + * fluctuate over time. + * + * @return an Iterator for the occupants in the group chat. + */ + public Iterator getOccupants() { + synchronized (occupantsMap) { + return Collections.unmodifiableList(new ArrayList(occupantsMap.keySet())).iterator(); + } + } + + /** + * Returns the presence info for a particular user, or <tt>null</tt> if the user + * is not in the room.<p> + * + * @param user the room occupant to search for his presence. The format of user must + * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch). + * @return the occupant's current presence, or <tt>null</tt> if the user is unavailable + * or if no presence information is available. + */ + public Presence getOccupantPresence(String user) { + return (Presence) occupantsMap.get(user); + } + + /** + * Returns the Occupant information for a particular occupant, or <tt>null</tt> if the + * user is not in the room. The Occupant object may include information such as full + * JID of the user as well as the role and affiliation of the user in the room.<p> + * + * @param user the room occupant to search for his presence. The format of user must + * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch). + * @return the Occupant or <tt>null</tt> if the user is unavailable (i.e. not in the room). + */ + public Occupant getOccupant(String user) { + Presence presence = (Presence) occupantsMap.get(user); + if (presence != null) { + return new Occupant(presence); + } + return null; + } + + /** + * Adds a packet listener that will be notified of any new Presence packets + * sent to the group chat. Using a listener is a suitable way to know when the list + * of occupants should be re-loaded due to any changes. + * + * @param listener a packet listener that will be notified of any presence packets + * sent to the group chat. + */ + public void addParticipantListener(PacketListener listener) { + connection.addPacketListener(listener, presenceFilter); + connectionListeners.add(listener); + } + + /** + * Remoces a packet listener that was being notified of any new Presence packets + * sent to the group chat. + * + * @param listener a packet listener that was being notified of any presence packets + * sent to the group chat. + */ + public void removeParticipantListener(PacketListener listener) { + connection.removePacketListener(listener); + connectionListeners.remove(listener); + } + + /** + * Returns a collection of <code>Affiliate</code> with the room owners. + * + * @return a collection of <code>Affiliate</code> with the room owners. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + public Collection getOwners() throws XMPPException { + return getAffiliatesByOwner("owner"); + } + + /** + * Returns a collection of <code>Affiliate</code> with the room administrators. + * + * @return a collection of <code>Affiliate</code> with the room administrators. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + public Collection getAdmins() throws XMPPException { + return getAffiliatesByOwner("admin"); + } + + /** + * Returns a collection of <code>Affiliate</code> with the room members. + * + * @return a collection of <code>Affiliate</code> with the room members. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + public Collection getMembers() throws XMPPException { + return getAffiliatesByAdmin("member"); + } + + /** + * Returns a collection of <code>Affiliate</code> with the room outcasts. + * + * @return a collection of <code>Affiliate</code> with the room outcasts. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + public Collection getOutcasts() throws XMPPException { + return getAffiliatesByAdmin("outcast"); + } + + /** + * Returns a collection of <code>Affiliate</code> that have the specified room affiliation + * sending a request in the owner namespace. + * + * @param affiliation the affiliation of the users in the room. + * @return a collection of <code>Affiliate</code> that have the specified room affiliation. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + private Collection getAffiliatesByOwner(String affiliation) throws XMPPException { + MUCOwner iq = new MUCOwner(); + iq.setTo(room); + iq.setType(IQ.Type.GET); + // Set the specified affiliation. This may request the list of owners/admins/members/outcasts. + MUCOwner.Item item = new MUCOwner.Item(affiliation); + iq.addItem(item); + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + MUCOwner answer = (MUCOwner) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + // Get the list of affiliates from the server's answer + List affiliates = new ArrayList(); + for (Iterator it = answer.getItems(); it.hasNext();) { + affiliates.add(new Affiliate((MUCOwner.Item) it.next())); + } + return affiliates; + } + + /** + * Returns a collection of <code>Affiliate</code> that have the specified room affiliation + * sending a request in the admin namespace. + * + * @param affiliation the affiliation of the users in the room. + * @return a collection of <code>Affiliate</code> that have the specified room affiliation. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + private Collection getAffiliatesByAdmin(String affiliation) throws XMPPException { + MUCAdmin iq = new MUCAdmin(); + iq.setTo(room); + iq.setType(IQ.Type.GET); + // Set the specified affiliation. This may request the list of owners/admins/members/outcasts. + MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null); + iq.addItem(item); + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + // Get the list of affiliates from the server's answer + List affiliates = new ArrayList(); + for (Iterator it = answer.getItems(); it.hasNext();) { + affiliates.add(new Affiliate((MUCAdmin.Item) it.next())); + } + return affiliates; + } + + /** + * Returns a collection of <code>Occupant</code> with the room moderators. + * + * @return a collection of <code>Occupant</code> with the room moderators. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + public Collection getModerators() throws XMPPException { + return getOccupants("moderator"); + } + + /** + * Returns a collection of <code>Occupant</code> with the room participants. + * + * @return a collection of <code>Occupant</code> with the room participants. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + public Collection getParticipants() throws XMPPException { + return getOccupants("participant"); + } + + /** + * Returns a collection of <code>Occupant</code> that have the specified room role. + * + * @param role the role of the occupant in the room. + * @return a collection of <code>Occupant</code> that have the specified room role. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + private Collection getOccupants(String role) throws XMPPException { + MUCAdmin iq = new MUCAdmin(); + iq.setTo(room); + iq.setType(IQ.Type.GET); + // Set the specified role. This may request the list of moderators/participants. + MUCAdmin.Item item = new MUCAdmin.Item(null, role); + iq.addItem(item); + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + // Get the list of participants from the server's answer + List participants = new ArrayList(); + for (Iterator it = answer.getItems(); it.hasNext();) { + participants.add(new Occupant((MUCAdmin.Item) it.next())); + } + return participants; + } + + /** + * Sends a message to the chat room. + * + * @param text the text of the message to send. + * @throws XMPPException if sending the message fails. + */ + public void sendMessage(String text) throws XMPPException { + Message message = new Message(room, Message.Type.GROUP_CHAT); + message.setBody(text); + connection.sendPacket(message); + } + + /** + * Returns a new Chat for sending private messages to a given room occupant. + * The Chat's occupant address is the room's JID (i.e. roomName@service/nick). The server + * service will change the 'from' address to the sender's room JID and delivering the message + * to the intended recipient's full JID. + * + * @param occupant occupant unique room JID (e.g. 'darkcave@macbeth.shakespeare.lit/Paul'). + * @return new Chat for sending private messages to a given room occupant. + */ + public Chat createPrivateChat(String occupant) { + return new Chat(connection, occupant); + } + + /** + * Creates a new Message to send to the chat room. + * + * @return a new Message addressed to the chat room. + */ + public Message createMessage() { + return new Message(room, Message.Type.GROUP_CHAT); + } + + /** + * Sends a Message to the chat room. + * + * @param message the message. + * @throws XMPPException if sending the message fails. + */ + public void sendMessage(Message message) throws XMPPException { + connection.sendPacket(message); + } + + /** + * Polls for and returns the next message, or <tt>null</tt> if there isn't + * a message immediately available. This method provides significantly different + * functionalty than the {@link #nextMessage()} method since it's non-blocking. + * In other words, the method call will always return immediately, whereas the + * nextMessage method will return only when a message is available (or after + * a specific timeout). + * + * @return the next message if one is immediately available and + * <tt>null</tt> otherwise. + */ + public Message pollMessage() { + return (Message) messageCollector.pollResult(); + } + + /** + * Returns the next available message in the chat. The method call will block + * (not return) until a message is available. + * + * @return the next message. + */ + public Message nextMessage() { + return (Message) messageCollector.nextResult(); + } + + /** + * Returns the next available message in the chat. The method call will block + * (not return) until a packet is available or the <tt>timeout</tt> has elapased. + * If the timeout elapses without a result, <tt>null</tt> will be returned. + * + * @param timeout the maximum amount of time to wait for the next message. + * @return the next message, or <tt>null</tt> if the timeout elapses without a + * message becoming available. + */ + public Message nextMessage(long timeout) { + return (Message) messageCollector.nextResult(timeout); + } + + /** + * Adds a packet listener that will be notified of any new messages in the + * group chat. Only "group chat" messages addressed to this group chat will + * be delivered to the listener. If you wish to listen for other packets + * that may be associated with this group chat, you should register a + * PacketListener directly with the XMPPConnection with the appropriate + * PacketListener. + * + * @param listener a packet listener. + */ + public void addMessageListener(PacketListener listener) { + connection.addPacketListener(listener, messageFilter); + connectionListeners.add(listener); + } + + /** + * Removes a packet listener that was being notified of any new messages in the + * multi user chat. Only "group chat" messages addressed to this multi user chat were + * being delivered to the listener. + * + * @param listener a packet listener. + */ + public void removeMessageListener(PacketListener listener) { + connection.removePacketListener(listener); + connectionListeners.remove(listener); + } + + /** + * Changes the subject within the room. As a default, only users with a role of "moderator" + * are allowed to change the subject in a room. Although some rooms may be configured to + * allow a mere participant or even a visitor to change the subject. + * + * @param subject the new room's subject to set. + * @throws XMPPException if someone without appropriate privileges attempts to change the + * room subject will throw an error with code 403 (i.e. Forbidden) + */ + public void changeSubject(final String subject) throws XMPPException { + Message message = new Message(room, Message.Type.GROUP_CHAT); + message.setSubject(subject); + // Wait for an error or confirmation message back from the server. + PacketFilter responseFilter = + new AndFilter( + new FromMatchesFilter(room), + new PacketTypeFilter(Message.class)); + responseFilter = new AndFilter(responseFilter, new PacketFilter() { + public boolean accept(Packet packet) { + Message msg = (Message) packet; + return subject.equals(msg.getSubject()); + } + }); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send change subject packet. + connection.sendPacket(message); + // Wait up to a certain number of seconds for a reply. + Message answer = + (Message) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + /** + * Notification message that the user has joined the room. + */ + private synchronized void userHasJoined() { + // Update the list of joined rooms through this connection + ArrayList rooms = (ArrayList)joinedRooms.get(connection); + if (rooms == null) { + rooms = new ArrayList(); + joinedRooms.put(connection, rooms); + } + rooms.add(room); + } + + /** + * Notification message that the user has left the room. + */ + private synchronized void userHasLeft() { + // Update the list of joined rooms through this connection + ArrayList rooms = (ArrayList)joinedRooms.get(connection); + if (rooms == null) { + return; + } + rooms.remove(room); + } + + /** + * Returns the MUCUser packet extension included in the packet or <tt>null</tt> if none. + * + * @param packet the packet that may include the MUCUser extension. + * @return the MUCUser found in the packet. + */ + private MUCUser getMUCUserExtension(Packet packet) { + if (packet != null) { + // Get the MUC User extension + return (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user"); + } + return null; + } + + /** + * Adds a listener that will be notified of changes in your status in the room + * such as the user being kicked, banned, or granted admin permissions. + * + * @param listener a user status listener. + */ + public void addUserStatusListener(UserStatusListener listener) { + synchronized (userStatusListeners) { + if (!userStatusListeners.contains(listener)) { + userStatusListeners.add(listener); + } + } + } + + /** + * Removes a listener that was being notified of changes in your status in the room + * such as the user being kicked, banned, or granted admin permissions. + * + * @param listener a user status listener. + */ + public void removeUserStatusListener(UserStatusListener listener) { + synchronized (userStatusListeners) { + userStatusListeners.remove(listener); + } + } + + private void fireUserStatusListeners(String methodName, Object[] params) { + UserStatusListener[] listeners = null; + synchronized (userStatusListeners) { + listeners = new UserStatusListener[userStatusListeners.size()]; + userStatusListeners.toArray(listeners); + } + // Get the classes of the method parameters + Class[] paramClasses = new Class[params.length]; + for (int i = 0; i < params.length; i++) { + paramClasses[i] = params[i].getClass(); + } + try { + // Get the method to execute based on the requested methodName and parameters classes + Method method = UserStatusListener.class.getDeclaredMethod(methodName, paramClasses); + for (int i = 0; i < listeners.length; i++) { + method.invoke(listeners[i], params); + } + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + /** + * Adds a listener that will be notified of changes in occupants status in the room + * such as the user being kicked, banned, or granted admin permissions. + * + * @param listener a participant status listener. + */ + public void addParticipantStatusListener(ParticipantStatusListener listener) { + synchronized (participantStatusListeners) { + if (!participantStatusListeners.contains(listener)) { + participantStatusListeners.add(listener); + } + } + } + + /** + * Removes a listener that was being notified of changes in occupants status in the room + * such as the user being kicked, banned, or granted admin permissions. + * + * @param listener a participant status listener. + */ + public void removeParticipantStatusListener(ParticipantStatusListener listener) { + synchronized (participantStatusListeners) { + participantStatusListeners.remove(listener); + } + } + + private void fireParticipantStatusListeners(String methodName, List params) { + ParticipantStatusListener[] listeners = null; + synchronized (participantStatusListeners) { + listeners = new ParticipantStatusListener[participantStatusListeners.size()]; + participantStatusListeners.toArray(listeners); + } + try { + // Get the method to execute based on the requested methodName and parameter + Class[] classes = new Class[params.size()]; + for (int i=0;i<params.size(); i++) { + classes[i] = String.class; + } + Method method = ParticipantStatusListener.class.getDeclaredMethod(methodName, classes); + for (int i = 0; i < listeners.length; i++) { + method.invoke(listeners[i], params.toArray()); + } + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + private void init() { + // Create a collector for incoming messages. + messageFilter = + new AndFilter( + new FromMatchesFilter(room), + new MessageTypeFilter(Message.Type.GROUP_CHAT)); + messageFilter = new AndFilter(messageFilter, new PacketFilter() { + public boolean accept(Packet packet) { + Message msg = (Message) packet; + return msg.getBody() != null; + } + }); + messageCollector = connection.createPacketCollector(messageFilter); + + // Create a listener for subject updates. + subjectFilter = + new AndFilter( + new FromMatchesFilter(room), + new MessageTypeFilter(Message.Type.GROUP_CHAT)); + subjectFilter = new AndFilter(subjectFilter, new PacketFilter() { + public boolean accept(Packet packet) { + Message msg = (Message) packet; + return msg.getSubject() != null; + } + }); + subjectListener = new PacketListener() { + public void processPacket(Packet packet) { + Message msg = (Message) packet; + // Update the room subject + subject = msg.getSubject(); + // Fire event for subject updated listeners + fireSubjectUpdatedListeners( + msg.getSubject(), + msg.getFrom()); + + } + }; + connection.addPacketListener(subjectListener, subjectFilter); + + // Create a listener for all presence updates. + presenceFilter = + new AndFilter(new FromMatchesFilter(room), new PacketTypeFilter(Presence.class)); + presenceListener = new PacketListener() { + public void processPacket(Packet packet) { + Presence presence = (Presence) packet; + String from = presence.getFrom(); + String myRoomJID = room + "/" + nickname; + boolean isUserStatusModification = presence.getFrom().equals(myRoomJID); + if (presence.getType() == Presence.Type.AVAILABLE) { + Presence oldPresence; + synchronized (occupantsMap) { + oldPresence = (Presence)occupantsMap.get(from); + occupantsMap.put(from, presence); + } + if (oldPresence != null) { + // Get the previous occupant's affiliation & role + MUCUser mucExtension = getMUCUserExtension(oldPresence); + String oldAffiliation = mucExtension.getItem().getAffiliation(); + String oldRole = mucExtension.getItem().getRole(); + // Get the new occupant's affiliation & role + mucExtension = getMUCUserExtension(presence); + String newAffiliation = mucExtension.getItem().getAffiliation(); + String newRole = mucExtension.getItem().getRole(); + // Fire role modification events + checkRoleModifications(oldRole, newRole, isUserStatusModification, from); + // Fire affiliation modification events + checkAffiliationModifications( + oldAffiliation, + newAffiliation, + isUserStatusModification, + from); + } + else { + // A new occupant has joined the room + if (!isUserStatusModification) { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("joined", params); + } + } + } + else if (presence.getType() == Presence.Type.UNAVAILABLE) { + synchronized (occupantsMap) { + occupantsMap.remove(from); + } + MUCUser mucUser = getMUCUserExtension(presence); + if (mucUser != null && mucUser.getStatus() != null) { + // Fire events according to the received presence code + checkPresenceCode( + mucUser.getStatus().getCode(), + presence.getFrom().equals(myRoomJID), + mucUser, + from); + } else { + // An occupant has left the room + if (!isUserStatusModification) { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("left", params); + } + } + } + } + }; + connection.addPacketListener(presenceListener, presenceFilter); + + // Listens for all messages that include a MUCUser extension and fire the invitation + // rejection listeners if the message includes an invitation rejection. + declinesFilter = new PacketExtensionFilter("x", "http://jabber.org/protocol/muc#user"); + declinesListener = new PacketListener() { + public void processPacket(Packet packet) { + // Get the MUC User extension + MUCUser mucUser = getMUCUserExtension(packet); + // Check if the MUCUser informs that the invitee has declined the invitation + if (mucUser.getDecline() != null && + ((Message) packet).getType() != Message.Type.ERROR) { + // Fire event for invitation rejection listeners + fireInvitationRejectionListeners( + mucUser.getDecline().getFrom(), + mucUser.getDecline().getReason()); + } + }; + }; + connection.addPacketListener(declinesListener, declinesFilter); + } + + /** + * Fires notification events if the role of a room occupant has changed. If the occupant that + * changed his role is your occupant then the <code>UserStatusListeners</code> added to this + * <code>MultiUserChat</code> will be fired. On the other hand, if the occupant that changed + * his role is not yours then the <code>ParticipantStatusListeners</code> added to this + * <code>MultiUserChat</code> will be fired. The following table shows the events that will + * be fired depending on the previous and new role of the occupant. + * + * <pre> + * <table border="1"> + * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr> + * + * <tr><td>None</td><td>Visitor</td><td>--</td></tr> + * <tr><td>Visitor</td><td>Participant</td><td>voiceGranted</td></tr> + * <tr><td>Participant</td><td>Moderator</td><td>moderatorGranted</td></tr> + * + * <tr><td>None</td><td>Participant</td><td>voiceGranted</td></tr> + * <tr><td>None</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr> + * <tr><td>Visitor</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr> + * + * <tr><td>Moderator</td><td>Participant</td><td>moderatorRevoked</td></tr> + * <tr><td>Participant</td><td>Visitor</td><td>voiceRevoked</td></tr> + * <tr><td>Visitor</td><td>None</td><td>kicked</td></tr> + * + * <tr><td>Moderator</td><td>Visitor</td><td>voiceRevoked + moderatorRevoked</td></tr> + * <tr><td>Moderator</td><td>None</td><td>kicked</td></tr> + * <tr><td>Participant</td><td>None</td><td>kicked</td></tr> + * </table> + * </pre> + * + * @param oldRole the previous role of the user in the room before receiving the new presence + * @param newRole the new role of the user in the room after receiving the new presence + * @param isUserModification whether the received presence is about your user in the room or not + * @param from the occupant whose role in the room has changed + * (e.g. room@conference.jabber.org/nick). + */ + private void checkRoleModifications( + String oldRole, + String newRole, + boolean isUserModification, + String from) { + // Voice was granted to a visitor + if (("visitor".equals(oldRole) || "none".equals(oldRole)) + && "participant".equals(newRole)) { + if (isUserModification) { + fireUserStatusListeners("voiceGranted", new Object[] {}); + } + else { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("voiceGranted", params); + } + } + // The participant's voice was revoked from the room + else if ( + "participant".equals(oldRole) + && ("visitor".equals(newRole) || "none".equals(newRole))) { + if (isUserModification) { + fireUserStatusListeners("voiceRevoked", new Object[] {}); + } + else { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("voiceRevoked", params); + } + } + // Moderator privileges were granted to a participant + if (!"moderator".equals(oldRole) && "moderator".equals(newRole)) { + if ("visitor".equals(oldRole) || "none".equals(oldRole)) { + if (isUserModification) { + fireUserStatusListeners("voiceGranted", new Object[] {}); + } + else { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("voiceGranted", params); + } + } + if (isUserModification) { + fireUserStatusListeners("moderatorGranted", new Object[] {}); + } + else { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("moderatorGranted", params); + } + } + // Moderator privileges were revoked from a participant + else if ("moderator".equals(oldRole) && !"moderator".equals(newRole)) { + if ("visitor".equals(newRole) || "none".equals(newRole)) { + if (isUserModification) { + fireUserStatusListeners("voiceRevoked", new Object[] {}); + } + else { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("voiceRevoked", params); + } + } + if (isUserModification) { + fireUserStatusListeners("moderatorRevoked", new Object[] {}); + } + else { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("moderatorRevoked", params); + } + } + } + + /** + * Fires notification events if the affiliation of a room occupant has changed. If the + * occupant that changed his affiliation is your occupant then the + * <code>UserStatusListeners</code> added to this <code>MultiUserChat</code> will be fired. + * On the other hand, if the occupant that changed his affiliation is not yours then the + * <code>ParticipantStatusListeners</code> added to this <code>MultiUserChat</code> will be + * fired. The following table shows the events that will be fired depending on the previous + * and new affiliation of the occupant. + * + * <pre> + * <table border="1"> + * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr> + * + * <tr><td>None</td><td>Member</td><td>membershipGranted</td></tr> + * <tr><td>Member</td><td>Admin</td><td>membershipRevoked + adminGranted</td></tr> + * <tr><td>Admin</td><td>Owner</td><td>adminRevoked + ownershipGranted</td></tr> + * + * <tr><td>None</td><td>Admin</td><td>adminGranted</td></tr> + * <tr><td>None</td><td>Owner</td><td>ownershipGranted</td></tr> + * <tr><td>Member</td><td>Owner</td><td>membershipRevoked + ownershipGranted</td></tr> + * + * <tr><td>Owner</td><td>Admin</td><td>ownershipRevoked + adminGranted</td></tr> + * <tr><td>Admin</td><td>Member</td><td>adminRevoked + membershipGranted</td></tr> + * <tr><td>Member</td><td>None</td><td>membershipRevoked</td></tr> + * + * <tr><td>Owner</td><td>Member</td><td>ownershipRevoked + membershipGranted</td></tr> + * <tr><td>Owner</td><td>None</td><td>ownershipRevoked</td></tr> + * <tr><td>Admin</td><td>None</td><td>adminRevoked</td></tr> + * <tr><td><i>Anyone</i></td><td>Outcast</td><td>banned</td></tr> + * </table> + * </pre> + * + * @param oldAffiliation the previous affiliation of the user in the room before receiving the + * new presence + * @param newAffiliation the new affiliation of the user in the room after receiving the new + * presence + * @param isUserModification whether the received presence is about your user in the room or not + * @param from the occupant whose role in the room has changed + * (e.g. room@conference.jabber.org/nick). + */ + private void checkAffiliationModifications( + String oldAffiliation, + String newAffiliation, + boolean isUserModification, + String from) { + // First check for revoked affiliation and then for granted affiliations. The idea is to + // first fire the "revoke" events and then fire the "grant" events. + + // The user's ownership to the room was revoked + if ("owner".equals(oldAffiliation) && !"owner".equals(newAffiliation)) { + if (isUserModification) { + fireUserStatusListeners("ownershipRevoked", new Object[] {}); + } + else { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("ownershipRevoked", params); + } + } + // The user's administrative privileges to the room were revoked + else if ("admin".equals(oldAffiliation) && !"admin".equals(newAffiliation)) { + if (isUserModification) { + fireUserStatusListeners("adminRevoked", new Object[] {}); + } + else { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("adminRevoked", params); + } + } + // The user's membership to the room was revoked + else if ("member".equals(oldAffiliation) && !"member".equals(newAffiliation)) { + if (isUserModification) { + fireUserStatusListeners("membershipRevoked", new Object[] {}); + } + else { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("membershipRevoked", params); + } + } + + // The user was granted ownership to the room + if (!"owner".equals(oldAffiliation) && "owner".equals(newAffiliation)) { + if (isUserModification) { + fireUserStatusListeners("ownershipGranted", new Object[] {}); + } + else { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("ownershipGranted", params); + } + } + // The user was granted administrative privileges to the room + else if (!"admin".equals(oldAffiliation) && "admin".equals(newAffiliation)) { + if (isUserModification) { + fireUserStatusListeners("adminGranted", new Object[] {}); + } + else { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("adminGranted", params); + } + } + // The user was granted membership to the room + else if (!"member".equals(oldAffiliation) && "member".equals(newAffiliation)) { + if (isUserModification) { + fireUserStatusListeners("membershipGranted", new Object[] {}); + } + else { + List params = new ArrayList(); + params.add(from); + fireParticipantStatusListeners("membershipGranted", params); + } + } + } + + /** + * Fires events according to the received presence code. + * + * @param code + * @param isUserModification + * @param mucUser + * @param from + */ + private void checkPresenceCode( + String code, + boolean isUserModification, + MUCUser mucUser, + String from) { + // Check if an occupant was kicked from the room + if ("307".equals(code)) { + // Check if this occupant was kicked + if (isUserModification) { + joined = false; + + fireUserStatusListeners( + "kicked", + new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()}); + + // Reset occupant information. + occupantsMap = new HashMap(); + nickname = null; + userHasLeft(); + } + else { + List params = new ArrayList(); + params.add(from); + params.add(mucUser.getItem().getActor()); + params.add(mucUser.getItem().getReason()); + fireParticipantStatusListeners("kicked", params); + } + } + // A user was banned from the room + else if ("301".equals(code)) { + // Check if this occupant was banned + if (isUserModification) { + joined = false; + + fireUserStatusListeners( + "banned", + new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()}); + + // Reset occupant information. + occupantsMap = new HashMap(); + nickname = null; + userHasLeft(); + } + else { + List params = new ArrayList(); + params.add(from); + params.add(mucUser.getItem().getActor()); + params.add(mucUser.getItem().getReason()); + fireParticipantStatusListeners("banned", params); + } + } + // A user's membership was revoked from the room + else if ("321".equals(code)) { + // Check if this occupant's membership was revoked + if (isUserModification) { + joined = false; + + fireUserStatusListeners("membershipRevoked", new Object[] {}); + + // Reset occupant information. + occupantsMap = new HashMap(); + nickname = null; + userHasLeft(); + } + } + // A occupant has changed his nickname in the room + else if ("303".equals(code)) { + List params = new ArrayList(); + params.add(from); + params.add(mucUser.getItem().getNick()); + fireParticipantStatusListeners("nicknameChanged", params); + } + } + + public void finalize() throws Throwable { + super.finalize(); + try { + if (connection != null) { + messageCollector.cancel(); + connection.removePacketListener(subjectListener); + connection.removePacketListener(presenceListener); + connection.removePacketListener(declinesListener); + // Remove all the PacketListeners added to the connection by this chat + for (Iterator it=connectionListeners.iterator(); it.hasNext();) { + connection.removePacketListener((PacketListener) it.next()); + } + } + } + catch (Exception e) {} + } + + /** + * An InvitationsMonitor monitors a given connection to detect room invitations. Every + * time the InvitationsMonitor detects a new invitation it will fire the invitation listeners. + * + * @author Gaston Dombiak + */ + private static class InvitationsMonitor implements ConnectionListener { + // We use a WeakHashMap so that the GC can collect the monitor when the + // connection is no longer referenced by any object. + private static Map monitors = new WeakHashMap(); + + private List invitationsListeners = new ArrayList(); + private XMPPConnection connection; + private PacketFilter invitationFilter; + private PacketListener invitationPacketListener; + + /** + * Returns a new or existing InvitationsMonitor for a given connection. + * + * @param conn the connection to monitor for room invitations. + * @return a new or existing InvitationsMonitor for a given connection. + */ + public static InvitationsMonitor getInvitationsMonitor(XMPPConnection conn) { + synchronized (monitors) { + if (!monitors.containsKey(conn)) { + // We need to use a WeakReference because the monitor references the + // connection and this could prevent the GC from collecting the monitor + // when no other object references the monitor + monitors.put(conn, new WeakReference(new InvitationsMonitor(conn))); + } + // Return the InvitationsMonitor that monitors the connection + return (InvitationsMonitor) ((WeakReference) monitors.get(conn)).get(); + } + } + + /** + * Creates a new InvitationsMonitor that will monitor invitations received + * on a given connection. + * + * @param connection the connection to monitor for possible room invitations + */ + private InvitationsMonitor(XMPPConnection connection) { + this.connection = connection; + } + + /** + * Adds a listener to invitation notifications. The listener will be fired anytime + * an invitation is received.<p> + * + * If this is the first monitor's listener then the monitor will be initialized in + * order to start listening to room invitations. + * + * @param listener an invitation listener. + */ + public void addInvitationListener(InvitationListener listener) { + synchronized (invitationsListeners) { + // If this is the first monitor's listener then initialize the listeners + // on the connection to detect room invitations + if (invitationsListeners.size() == 0) { + init(); + } + if (!invitationsListeners.contains(listener)) { + invitationsListeners.add(listener); + } + } + } + + /** + * Removes a listener to invitation notifications. The listener will be fired anytime + * an invitation is received.<p> + * + * If there are no more listeners to notifiy for room invitations then the monitor will + * be stopped. As soon as a new listener is added to the monitor, the monitor will resume + * monitoring the connection for new room invitations. + * + * @param listener an invitation listener. + */ + public void removeInvitationListener(InvitationListener listener) { + synchronized (invitationsListeners) { + if (invitationsListeners.contains(listener)) { + invitationsListeners.remove(listener); + } + // If there are no more listeners to notifiy for room invitations + // then proceed to cancel/release this monitor + if (invitationsListeners.size() == 0) { + cancel(); + } + } + } + + /** + * Fires invitation listeners. + */ + private void fireInvitationListeners(String room, String inviter, String reason, String password, + Message message) { + InvitationListener[] listeners = null; + synchronized (invitationsListeners) { + listeners = new InvitationListener[invitationsListeners.size()]; + invitationsListeners.toArray(listeners); + } + for (int i = 0; i < listeners.length; i++) { + listeners[i].invitationReceived(connection, room, inviter, reason, password, message); + } + } + + public void connectionClosed() { + cancel(); + } + + public void connectionClosedOnError(Exception e) { + cancel(); + } + + /** + * Initializes the listeners to detect received room invitations and to detect when the + * connection gets closed. As soon as a room invitation is received the invitations + * listeners will be fired. When the connection gets closed the monitor will remove + * his listeners on the connection. + */ + private void init() { + // Listens for all messages that include a MUCUser extension and fire the invitation + // listeners if the message includes an invitation. + invitationFilter = + new PacketExtensionFilter("x", "http://jabber.org/protocol/muc#user"); + invitationPacketListener = new PacketListener() { + public void processPacket(Packet packet) { + // Get the MUCUser extension + MUCUser mucUser = + (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user"); + // Check if the MUCUser extension includes an invitation + if (mucUser.getInvite() != null && + ((Message) packet).getType() != Message.Type.ERROR) { + // Fire event for invitation listeners + fireInvitationListeners(packet.getFrom(), mucUser.getInvite().getFrom(), + mucUser.getInvite().getReason(), mucUser.getPassword(), (Message) packet); + } + }; + }; + connection.addPacketListener(invitationPacketListener, invitationFilter); + // Add a listener to detect when the connection gets closed in order to + // cancel/release this monitor + connection.addConnectionListener(this); + } + + /** + * Cancels all the listeners that this InvitationsMonitor has added to the connection. + */ + private void cancel() { + connection.removePacketListener(invitationPacketListener); + connection.removeConnectionListener(this); + } + + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Occupant.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Occupant.java new file mode 100644 index 000000000..e82de84ad --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/Occupant.java @@ -0,0 +1,104 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +import org.jivesoftware.smackx.packet.MUCAdmin; +import org.jivesoftware.smackx.packet.MUCUser; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.util.StringUtils; + +/** + * Represents the information about an occupant in a given room. The information will always have + * the affiliation and role of the occupant in the room. The full JID and nickname are optional. + * + * @author Gaston Dombiak + */ +public class Occupant { + // Fields that must have a value + private String affiliation; + private String role; + // Fields that may have a value + private String jid; + private String nick; + + Occupant(MUCAdmin.Item item) { + super(); + this.jid = item.getJid(); + this.affiliation = item.getAffiliation(); + this.role = item.getRole(); + this.nick = item.getNick(); + } + + Occupant(Presence presence) { + super(); + MUCUser mucUser = (MUCUser) presence.getExtension("x", + "http://jabber.org/protocol/muc#user"); + MUCUser.Item item = mucUser.getItem(); + this.jid = item.getJid(); + this.affiliation = item.getAffiliation(); + this.role = item.getRole(); + // Get the nickname from the FROM attribute of the presence + this.nick = StringUtils.parseResource(presence.getFrom()); + } + + /** + * Returns the full JID of the occupant. If this information was extracted from a presence and + * the room is semi or full-anonymous then the answer will be null. On the other hand, if this + * information was obtained while maintaining the voice list or the moderator list then we will + * always have a full JID. + * + * @return the full JID of the occupant. + */ + public String getJid() { + return jid; + } + + /** + * Returns the affiliation of the occupant. Possible affiliations are: "owner", "admin", + * "member", "outcast". This information will always be available. + * + * @return the affiliation of the occupant. + */ + public String getAffiliation() { + return affiliation; + } + + /** + * Returns the current role of the occupant in the room. This information will always be + * available. + * + * @return the current role of the occupant in the room. + */ + public String getRole() { + return role; + } + + /** + * Returns the current nickname of the occupant in the room. If this information was extracted + * from a presence then the answer will be null. + * + * @return the current nickname of the occupant in the room or null if this information was + * obtained from a presence. + */ + public String getNick() { + return nick; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/ParticipantStatusListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/ParticipantStatusListener.java new file mode 100644 index 000000000..4035b2804 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/ParticipantStatusListener.java @@ -0,0 +1,179 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +/** + * A listener that is fired anytime a participant's status in a room is changed, such as the + * user being kicked, banned, or granted admin permissions. + * + * @author Gaston Dombiak + */ +public interface ParticipantStatusListener { + + /** + * Called when a new room occupant has joined the room. Note: Take in consideration that when + * you join a room you will receive the list of current occupants in the room. This message will + * be sent for each occupant. + * + * @param participant the participant that has just joined the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void joined(String participant); + + /** + * Called when a room occupant has left the room on its own. This means that the occupant was + * neither kicked nor banned from the room. + * + * @param participant the participant that has left the room on its own. + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void left(String participant); + + /** + * Called when a room participant has been kicked from the room. This means that the kicked + * participant is no longer participating in the room. + * + * @param participant the participant that was kicked from the room + * (e.g. room@conference.jabber.org/nick). + * @param actor the moderator that kicked the occupant from the room (e.g. user@host.org). + * @param reason the reason provided by the actor to kick the occupant from the room. + */ + public abstract void kicked(String participant, String actor, String reason); + + /** + * Called when a moderator grants voice to a visitor. This means that the visitor + * can now participate in the moderated room sending messages to all occupants. + * + * @param participant the participant that was granted voice in the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void voiceGranted(String participant); + + /** + * Called when a moderator revokes voice from a participant. This means that the participant + * in the room was able to speak and now is a visitor that can't send messages to the room + * occupants. + * + * @param participant the participant that was revoked voice from the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void voiceRevoked(String participant); + + /** + * Called when an administrator or owner banned a participant from the room. This means that + * banned participant will no longer be able to join the room unless the ban has been removed. + * + * @param participant the participant that was banned from the room + * (e.g. room@conference.jabber.org/nick). + * @param actor the administrator that banned the occupant (e.g. user@host.org). + * @param reason the reason provided by the administrator to ban the occupant. + */ + public abstract void banned(String participant, String actor, String reason); + + /** + * Called when an administrator grants a user membership to the room. This means that the user + * will be able to join the members-only room. + * + * @param participant the participant that was granted membership in the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void membershipGranted(String participant); + + /** + * Called when an administrator revokes a user membership to the room. This means that the + * user will not be able to join the members-only room. + * + * @param participant the participant that was revoked membership from the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void membershipRevoked(String participant); + + /** + * Called when an administrator grants moderator privileges to a user. This means that the user + * will be able to kick users, grant and revoke voice, invite other users, modify room's + * subject plus all the partcipants privileges. + * + * @param participant the participant that was granted moderator privileges in the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void moderatorGranted(String participant); + + /** + * Called when an administrator revokes moderator privileges from a user. This means that the + * user will no longer be able to kick users, grant and revoke voice, invite other users, + * modify room's subject plus all the partcipants privileges. + * + * @param participant the participant that was revoked moderator privileges in the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void moderatorRevoked(String participant); + + /** + * Called when an owner grants a user ownership on the room. This means that the user + * will be able to change defining room features as well as perform all administrative + * functions. + * + * @param participant the participant that was granted ownership on the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void ownershipGranted(String participant); + + /** + * Called when an owner revokes a user ownership on the room. This means that the user + * will no longer be able to change defining room features as well as perform all + * administrative functions. + * + * @param participant the participant that was revoked ownership on the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void ownershipRevoked(String participant); + + /** + * Called when an owner grants administrator privileges to a user. This means that the user + * will be able to perform administrative functions such as banning users and edit moderator + * list. + * + * @param participant the participant that was granted administrator privileges + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void adminGranted(String participant); + + /** + * Called when an owner revokes administrator privileges from a user. This means that the user + * will no longer be able to perform administrative functions such as banning users and edit + * moderator list. + * + * @param participant the participant that was revoked administrator privileges + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void adminRevoked(String participant); + + /** + * Called when a participant changed his/her nickname in the room. The new participant's + * nickname will be informed with the next available presence. + * + * @param participant the participant that was revoked administrator privileges + * (e.g. room@conference.jabber.org/nick). + * @param newNickname the new nickname that the participant decided to use. + */ + public abstract void nicknameChanged(String participant, String newNickname); + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/RoomInfo.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/RoomInfo.java new file mode 100644 index 000000000..694dd38d8 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/RoomInfo.java @@ -0,0 +1,184 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.Form; + +/** + * Represents the room information that was discovered using Service Discovery. It's possible to + * obtain information about a room before joining the room but only for rooms that are public (i.e. + * rooms that may be discovered). + * + * @author Gaston Dombiak + */ +public class RoomInfo { + + /** + * JID of the room. The node of the JID is commonly used as the ID of the room or name. + */ + private String room; + /** + * Description of the room. + */ + private String description = ""; + /** + * Last known subject of the room. + */ + private String subject = ""; + /** + * Current number of occupants in the room. + */ + private int occupantsCount = -1; + /** + * A room is considered members-only if an invitation is required in order to enter the room. + * Any user that is not a member of the room won't be able to join the room unless the user + * decides to register with the room (thus becoming a member). + */ + private boolean membersOnly; + /** + * Moderated rooms enable only participants to speak. Users that join the room and aren't + * participants can't speak (they are just visitors). + */ + private boolean moderated; + /** + * Every presence packet can include the JID of every occupant unless the owner deactives this + * configuration. + */ + private boolean nonanonymous; + /** + * Indicates if users must supply a password to join the room. + */ + private boolean passwordProtected; + /** + * Persistent rooms are saved to the database to make sure that rooms configurations can be + * restored in case the server goes down. + */ + private boolean persistent; + + RoomInfo(DiscoverInfo info) { + super(); + this.room = info.getFrom(); + // Get the information based on the discovered features + this.membersOnly = info.containsFeature("muc_membersonly"); + this.moderated = info.containsFeature("muc_moderated"); + this.nonanonymous = info.containsFeature("muc_nonanonymous"); + this.passwordProtected = info.containsFeature("muc_passwordprotected"); + this.persistent = info.containsFeature("muc_persistent"); + // Get the information based on the discovered extended information + Form form = Form.getFormFrom(info); + if (form != null) { + this.description = + (String) form.getField("muc#roominfo_description").getValues().next(); + this.subject = (String) form.getField("muc#roominfo_subject").getValues().next(); + this.occupantsCount = + Integer.parseInt((String) form.getField("muc#roominfo_occupants").getValues() + .next()); + } + } + + /** + * Returns the JID of the room whose information was discovered. + * + * @return the JID of the room whose information was discovered. + */ + public String getRoom() { + return room; + } + + /** + * Returns the discovered description of the room. + * + * @return the discovered description of the room. + */ + public String getDescription() { + return description; + } + + /** + * Returns the discovered subject of the room. The subject may be empty if the room does not + * have a subject. + * + * @return the discovered subject of the room. + */ + public String getSubject() { + return subject; + } + + /** + * Returns the discovered number of occupants that are currently in the room. If this + * information was not discovered (i.e. the server didn't send it) then a value of -1 will be + * returned. + * + * @return the number of occupants that are currently in the room or -1 if that information was + * not provided by the server. + */ + public int getOccupantsCount() { + return occupantsCount; + } + + /** + * Returns true if the room has restricted the access so that only members may enter the room. + * + * @return true if the room has restricted the access so that only members may enter the room. + */ + public boolean isMembersOnly() { + return membersOnly; + } + + /** + * Returns true if the room enabled only participants to speak. Occupants with a role of + * visitor won't be able to speak in the room. + * + * @return true if the room enabled only participants to speak. + */ + public boolean isModerated() { + return moderated; + } + + /** + * Returns true if presence packets will include the JID of every occupant. + * + * @return true if presence packets will include the JID of every occupant. + */ + public boolean isNonanonymous() { + return nonanonymous; + } + + /** + * Returns true if users musy provide a valid password in order to join the room. + * + * @return true if users musy provide a valid password in order to join the room. + */ + public boolean isPasswordProtected() { + return passwordProtected; + } + + /** + * Returns true if the room will persist after the last occupant have left the room. + * + * @return true if the room will persist after the last occupant have left the room. + */ + public boolean isPersistent() { + return persistent; + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/SubjectUpdatedListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/SubjectUpdatedListener.java new file mode 100644 index 000000000..a966d7f19 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/SubjectUpdatedListener.java @@ -0,0 +1,38 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +/** + * A listener that is fired anytime a MUC room changes its subject. + * + * @author Gaston Dombiak + */ +public interface SubjectUpdatedListener { + + /** + * Called when a MUC room has changed its subject. + * + * @param subject the new room's subject. + * @param from the user that changed the room's subject (e.g. room@conference.jabber.org/nick). + */ + public abstract void subjectUpdated(String subject, String from); + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/UserStatusListener.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/UserStatusListener.java new file mode 100644 index 000000000..6d243419a --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/UserStatusListener.java @@ -0,0 +1,127 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.muc; + +/** + * A listener that is fired anytime your participant's status in a room is changed, such as the + * user being kicked, banned, or granted admin permissions. + * + * @author Gaston Dombiak + */ +public interface UserStatusListener { + + /** + * Called when a moderator kicked your user from the room. This means that you are no longer + * participanting in the room. + * + * @param actor the moderator that kicked your user from the room (e.g. user@host.org). + * @param reason the reason provided by the actor to kick you from the room. + */ + public abstract void kicked(String actor, String reason); + + /** + * Called when a moderator grants voice to your user. This means that you were a visitor in + * the moderated room before and now you can participate in the room by sending messages to + * all occupants. + * + */ + public abstract void voiceGranted(); + + /** + * Called when a moderator revokes voice from your user. This means that you were a + * participant in the room able to speak and now you are a visitor that can't send + * messages to the room occupants. + * + */ + public abstract void voiceRevoked(); + + /** + * Called when an administrator or owner banned your user from the room. This means that you + * will no longer be able to join the room unless the ban has been removed. + * + * @param actor the administrator that banned your user (e.g. user@host.org). + * @param reason the reason provided by the administrator to banned you. + */ + public abstract void banned(String actor, String reason); + + /** + * Called when an administrator grants your user membership to the room. This means that you + * will be able to join the members-only room. + * + */ + public abstract void membershipGranted(); + + /** + * Called when an administrator revokes your user membership to the room. This means that you + * will not be able to join the members-only room. + * + */ + public abstract void membershipRevoked(); + + /** + * Called when an administrator grants moderator privileges to your user. This means that you + * will be able to kick users, grant and revoke voice, invite other users, modify room's + * subject plus all the partcipants privileges. + * + */ + public abstract void moderatorGranted(); + + /** + * Called when an administrator revokes moderator privileges from your user. This means that + * you will no longer be able to kick users, grant and revoke voice, invite other users, + * modify room's subject plus all the partcipants privileges. + * + */ + public abstract void moderatorRevoked(); + + /** + * Called when an owner grants to your user ownership on the room. This means that you + * will be able to change defining room features as well as perform all administrative + * functions. + * + */ + public abstract void ownershipGranted(); + + /** + * Called when an owner revokes from your user ownership on the room. This means that you + * will no longer be able to change defining room features as well as perform all + * administrative functions. + * + */ + public abstract void ownershipRevoked(); + + /** + * Called when an owner grants administrator privileges to your user. This means that you + * will be able to perform administrative functions such as banning users and edit moderator + * list. + * + */ + public abstract void adminGranted(); + + /** + * Called when an owner revokes administrator privileges from your user. This means that you + * will no longer be able to perform administrative functions such as banning users and edit + * moderator list. + * + */ + public abstract void adminRevoked(); + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/package.html new file mode 100644 index 000000000..dcfaeaace --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/muc/package.html @@ -0,0 +1 @@ +<body>Classes and Interfaces that implement Multi-User Chat (MUC).</body>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/package.html new file mode 100644 index 000000000..d574a2a4d --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/package.html @@ -0,0 +1 @@ +<body>Smack extensions API.</body>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Bytestream.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Bytestream.java new file mode 100644 index 000000000..57136218d --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Bytestream.java @@ -0,0 +1,481 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.PacketExtension; + +import java.util.*; + +/** + * A packet representing part of a Socks5 Bytestream negotiation. + * + * @author Alexander Wenckus + */ +public class Bytestream extends IQ { + + private String sessionID; + + private Mode mode = Mode.TCP; + + private final List streamHosts = new ArrayList(); + + private StreamHostUsed usedHost; + + private Activate toActivate; + + /** + * The default constructor + */ + public Bytestream() { + super(); + } + + /** + * A constructor where the session ID can be specified. + * + * @param SID The session ID related to the negotiation. + * @see #setSessionID(String) + */ + public Bytestream(final String SID) { + super(); + setSessionID(SID); + } + + /** + * Set the session ID related to the Byte Stream. The session ID is a unique + * identifier used to differentiate between stream negotations. + * + * @param sessionID + */ + public void setSessionID(final String sessionID) { + this.sessionID = sessionID; + } + + /** + * Returns the session ID related to the Byte Stream negotiation. + * + * @return Returns the session ID related to the Byte Stream negotiation. + * @see #setSessionID(String) + */ + public String getSessionID() { + return sessionID; + } + + /** + * Set the transport mode. This should be put in the initiation of the + * interaction. + * + * @param mode + * @see Mode + */ + public void setMode(final Mode mode) { + this.mode = mode; + } + + /** + * Returns the transport mode. + * + * @return Returns the transport mode. + * @see #setMode(Mode) + */ + public Mode getMode() { + return mode; + } + + /** + * Adds a potential stream host that the remote user can connect to to + * receive the file. + * + * @param JID The jabber ID of the stream host. + * @param address The internet address of the stream host. + * @return The added stream host. + */ + public StreamHost addStreamHost(final String JID, final String address) { + return addStreamHost(JID, address, 0); + } + + /** + * Adds a potential stream host that the remote user can connect to to + * receive the file. + * + * @param JID The jabber ID of the stream host. + * @param address The internet address of the stream host. + * @param port The port on which the remote host is seeking connections. + * @return The added stream host. + */ + public StreamHost addStreamHost(final String JID, final String address, + final int port) { + StreamHost host = new StreamHost(JID, address); + host.setPort(port); + addStreamHost(host); + + return host; + } + + /** + * Adds a potential stream host that the remote user can transfer the file + * through. + * + * @param host The potential stream host. + */ + public void addStreamHost(final StreamHost host) { + streamHosts.add(host); + } + + /** + * Returns the list of stream hosts contained in the packet. + * + * @return Returns the list of stream hosts contained in the packet. + */ + public Collection getStreamHosts() { + return Collections.unmodifiableCollection(streamHosts); + } + + /** + * Returns the stream host related to the given jabber ID, or null if there + * is none. + * + * @param JID The jabber ID of the desired stream host. + * @return Returns the stream host related to the given jabber ID, or null + * if there is none. + */ + public StreamHost getStreamHost(final String JID) { + StreamHost host; + for (Iterator it = streamHosts.iterator(); it.hasNext();) { + host = (StreamHost) it.next(); + if (host.getJID().equals(JID)) { + return host; + } + } + + return null; + } + + /** + * Returns the count of stream hosts contained in this packet. + * + * @return Returns the count of stream hosts contained in this packet. + */ + public int countStreamHosts() { + return streamHosts.size(); + } + + /** + * Upon connecting to the stream host the target of the stream replys to the + * initiator with the jabber id of the Socks5 host that they used. + * + * @param JID The jabber ID of the used host. + */ + public void setUsedHost(final String JID) { + this.usedHost = new StreamHostUsed(JID); + } + + /** + * Returns the Socks5 host connected to by the remote user. + * + * @return Returns the Socks5 host connected to by the remote user. + */ + public StreamHostUsed getUsedHost() { + return usedHost; + } + + /** + * Returns the activate element of the packet sent to the proxy host to + * verify the identity of the initiator and match them to the appropriate + * stream. + * + * @return Returns the activate element of the packet sent to the proxy host + * to verify the identity of the initiator and match them to the + * appropriate stream. + */ + public Activate getToActivate() { + return toActivate; + } + + /** + * Upon the response from the target of the used host the activate packet is + * sent to the Socks5 proxy. The proxy will activate the stream or return an + * error after verifying the identity of the initiator, using the activate + * packet. + * + * @param targetID The jabber ID of the target of the file transfer. + */ + public void setToActivate(final String targetID) { + this.toActivate = new Activate(targetID); + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + + buf.append("<query xmlns=\"http://jabber.org/protocol/bytestreams\""); + if (this.getType().equals(IQ.Type.SET)) { + if (getSessionID() != null) + buf.append(" sid=\"").append(getSessionID()).append("\""); + if (getMode() != null) + buf.append(" mode = \"").append(getMode()).append("\""); + buf.append(">"); + if (getToActivate() == null) { + for (Iterator it = getStreamHosts().iterator(); it.hasNext();) + buf.append(((StreamHost) it.next()).toXML()); + } + else { + buf.append(getToActivate().toXML()); + } + } + else if (this.getType().equals(IQ.Type.RESULT)) { + buf.append(">"); + if (getUsedHost() != null) + buf.append(getUsedHost().toXML()); + } + else { + return null; + } + buf.append("</query>"); + + return buf.toString(); + } + + /** + * Packet extension that represents a potential Socks5 proxy for the file + * transfer. Stream hosts are forwared to the target of the file transfer + * who then chooses and connects to one. + * + * @author Alexander Wenckus + */ + public static class StreamHost implements PacketExtension { + + public static String NAMESPACE = ""; + + public static String ELEMENTNAME = "streamhost"; + + private final String JID; + + private final String addy; + + private int port = 0; + + /** + * Default constructor. + * + * @param JID The jabber ID of the stream host. + * @param address The internet address of the stream host. + */ + public StreamHost(final String JID, final String address) { + this.JID = JID; + this.addy = address; + } + + /** + * Returns the jabber ID of the stream host. + * + * @return Returns the jabber ID of the stream host. + */ + public String getJID() { + return JID; + } + + /** + * Returns the internet address of the stream host. + * + * @return Returns the internet address of the stream host. + */ + public String getAddress() { + return addy; + } + + /** + * Sets the port of the stream host. + * + * @param port The port on which the potential stream host would accept + * the connection. + */ + public void setPort(final int port) { + this.port = port; + } + + /** + * Returns the port on which the potential stream host would accept the + * connection. + * + * @return Returns the port on which the potential stream host would + * accept the connection. + */ + public int getPort() { + return port; + } + + public String getNamespace() { + return NAMESPACE; + } + + public String getElementName() { + return ELEMENTNAME; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + + buf.append("<").append(getElementName()).append(" "); + buf.append("jid=\"").append(getJID()).append("\" "); + buf.append("host=\"").append(getAddress()).append("\" "); + if (getPort() != 0) + buf.append("port=\"").append(getPort()).append("\""); + else + buf.append("zeroconf=\"_jabber.bytestreams\""); + buf.append("/>"); + + return buf.toString(); + } + } + + /** + * After selected a Socks5 stream host and successfully connecting, the + * target of the file transfer returns a byte stream packet with the stream + * host used extension. + * + * @author Alexander Wenckus + */ + public static class StreamHostUsed implements PacketExtension { + + public String NAMESPACE = ""; + + public static String ELEMENTNAME = "streamhost-used"; + + private final String JID; + + /** + * Default constructor. + * + * @param JID The jabber ID of the selected stream host. + */ + public StreamHostUsed(final String JID) { + this.JID = JID; + } + + /** + * Returns the jabber ID of the selected stream host. + * + * @return Returns the jabber ID of the selected stream host. + */ + public String getJID() { + return JID; + } + + public String getNamespace() { + return NAMESPACE; + } + + public String getElementName() { + return ELEMENTNAME; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" "); + buf.append("jid=\"").append(getJID()).append("\" "); + buf.append("/>"); + return buf.toString(); + } + } + + /** + * The packet sent by the stream initiator to the stream proxy to activate + * the connection. + * + * @author Alexander Wenckus + */ + public static class Activate implements PacketExtension { + + public String NAMESPACE = ""; + + public static String ELEMENTNAME = "activate"; + + private final String target; + + /** + * Default constructor specifying the target of the stream. + * + * @param target The target of the stream. + */ + public Activate(final String target) { + this.target = target; + } + + /** + * Returns the target of the activation. + * + * @return Returns the target of the activation. + */ + public String getTarget() { + return target; + } + + public String getNamespace() { + return NAMESPACE; + } + + public String getElementName() { + return ELEMENTNAME; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(">"); + buf.append(getTarget()); + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + } + + /** + * The stream can be either a TCP stream or a UDP stream. + * + * @author Alexander Wenckus + */ + public static class Mode { + + /** + * A TCP based stream. + */ + public static Mode TCP = new Mode("tcp"); + + /** + * A UDP based stream. + */ + public static Mode UDP = new Mode("udp"); + + private final String modeString; + + private Mode(final String mode) { + this.modeString = mode; + } + + public String toString() { + return modeString; + } + + public boolean equals(final Object obj) { + if (!(obj instanceof Mode)) + return false; + return modeString.equals(((Mode) obj).modeString); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DataForm.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DataForm.java new file mode 100644 index 000000000..82b2939cc --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DataForm.java @@ -0,0 +1,296 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.FormField; + +/** + * Represents a form that could be use for gathering data as well as for reporting data + * returned from a search. + * + * @author Gaston Dombiak + */ +public class DataForm implements PacketExtension { + + private String type; + private String title; + private List instructions = new ArrayList(); + private ReportedData reportedData; + private List items = new ArrayList(); + private List fields = new ArrayList(); + + public DataForm(String type) { + this.type = type; + } + + /** + * Returns the meaning of the data within the context. The data could be part of a form + * to fill out, a form submission or data results.<p> + * + * Possible form types are: + * <ul> + * <li>form -> This packet contains a form to fill out. Display it to the user (if your + * program can).</li> + * <li>submit -> The form is filled out, and this is the data that is being returned from + * the form.</li> + * <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li> + * <li>result -> Data results being returned from a search, or some other query.</li> + * </ul> + * + * @return the form's type. + */ + public String getType() { + return type; + } + + /** + * Returns the description of the data. It is similar to the title on a web page or an X + * window. You can put a <title/> on either a form to fill out, or a set of data results. + * + * @return description of the data. + */ + public String getTitle() { + return title; + } + + /** + * Returns an Iterator for the list of instructions that explain how to fill out the form and + * what the form is about. The dataform could include multiple instructions since each + * instruction could not contain newlines characters. Join the instructions together in order + * to show them to the user. + * + * @return an Iterator for the list of instructions that explain how to fill out the form. + */ + public Iterator getInstructions() { + synchronized (instructions) { + return Collections.unmodifiableList(new ArrayList(instructions)).iterator(); + } + } + + /** + * Returns the fields that will be returned from a search. + * + * @return fields that will be returned from a search. + */ + public ReportedData getReportedData() { + return reportedData; + } + + /** + * Returns an Iterator for the items returned from a search. + * + * @return an Iterator for the items returned from a search. + */ + public Iterator getItems() { + synchronized (items) { + return Collections.unmodifiableList(new ArrayList(items)).iterator(); + } + } + + /** + * Returns an Iterator for the fields that are part of the form. + * + * @return an Iterator for the fields that are part of the form. + */ + public Iterator getFields() { + synchronized (fields) { + return Collections.unmodifiableList(new ArrayList(fields)).iterator(); + } + } + + public String getElementName() { + return "x"; + } + + public String getNamespace() { + return "jabber:x:data"; + } + + /** + * Sets the description of the data. It is similar to the title on a web page or an X window. + * You can put a <title/> on either a form to fill out, or a set of data results. + * + * @param title description of the data. + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * Sets the list of instructions that explain how to fill out the form and what the form is + * about. The dataform could include multiple instructions since each instruction could not + * contain newlines characters. + * + * @param instructions list of instructions that explain how to fill out the form. + */ + public void setInstructions(List instructions) { + this.instructions = instructions; + } + + /** + * Sets the fields that will be returned from a search. + * + * @param reportedData the fields that will be returned from a search. + */ + public void setReportedData(ReportedData reportedData) { + this.reportedData = reportedData; + } + + /** + * Adds a new field as part of the form. + * + * @param field the field to add to the form. + */ + public void addField(FormField field) { + synchronized (fields) { + fields.add(field); + } + } + + /** + * Adds a new instruction to the list of instructions that explain how to fill out the form + * and what the form is about. The dataform could include multiple instructions since each + * instruction could not contain newlines characters. + * + * @param instruction the new instruction that explain how to fill out the form. + */ + public void addInstruction(String instruction) { + synchronized (instructions) { + instructions.add(instruction); + } + } + + /** + * Adds a new item returned from a search. + * + * @param item the item returned from a search. + */ + public void addItem(Item item) { + synchronized (items) { + items.add(item); + } + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\" type=\"" + getType() +"\">"); + if (getTitle() != null) { + buf.append("<title>").append(getTitle()).append("</title>"); + } + for (Iterator it=getInstructions(); it.hasNext();) { + buf.append("<instructions>").append(it.next()).append("</instructions>"); + } + // Append the list of fields returned from a search + if (getReportedData() != null) { + buf.append(getReportedData().toXML()); + } + // Loop through all the items returned from a search and append them to the string buffer + for (Iterator i = getItems(); i.hasNext();) { + Item item = (Item) i.next(); + buf.append(item.toXML()); + } + // Loop through all the form fields and append them to the string buffer + for (Iterator i = getFields(); i.hasNext();) { + FormField field = (FormField) i.next(); + buf.append(field.toXML()); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + /** + * + * Represents the fields that will be returned from a search. This information is useful when + * you try to use the jabber:iq:search namespace to return dynamic form information. + * + * @author Gaston Dombiak + */ + public static class ReportedData { + private List fields = new ArrayList(); + + public ReportedData(List fields) { + this.fields = fields; + } + + /** + * Returns the fields returned from a search. + * + * @return the fields returned from a search. + */ + public Iterator getFields() { + return Collections.unmodifiableList(new ArrayList(fields)).iterator(); + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<reported>"); + // Loop through all the form items and append them to the string buffer + for (Iterator i = getFields(); i.hasNext();) { + FormField field = (FormField) i.next(); + buf.append(field.toXML()); + } + buf.append("</reported>"); + return buf.toString(); + } + } + + /** + * + * Represents items of reported data. + * + * @author Gaston Dombiak + */ + public static class Item { + private List fields = new ArrayList(); + + public Item(List fields) { + this.fields = fields; + } + + /** + * Returns the fields that define the data that goes with the item. + * + * @return the fields that define the data that goes with the item. + */ + public Iterator getFields() { + return Collections.unmodifiableList(new ArrayList(fields)).iterator(); + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<item>"); + // Loop through all the form items and append them to the string buffer + for (Iterator i = getFields(); i.hasNext();) { + FormField field = (FormField) i.next(); + buf.append(field.toXML()); + } + buf.append("</item>"); + return buf.toString(); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DefaultPrivateData.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DefaultPrivateData.java new file mode 100644 index 000000000..c000d2eed --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DefaultPrivateData.java @@ -0,0 +1,137 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import java.util.Map; +import java.util.Iterator; +import java.util.Collections; +import java.util.HashMap; + +/** + * Default implementation of the PrivateData interface. Unless a PrivateDataProvider + * is registered with the PrivateDataManager class, instances of this class will be + * returned when getting private data.<p> + * + * This class provides a very simple representation of an XML sub-document. Each element + * is a key in a Map with its CDATA being the value. For example, given the following + * XML sub-document: + * + * <pre> + * <foo xmlns="http://bar.com"> + * <color>blue</color> + * <food>pizza</food> + * </foo></pre> + * + * In this case, getValue("color") would return "blue", and getValue("food") would + * return "pizza". This parsing mechanism mechanism is very simplistic and will not work + * as desired in all cases (for example, if some of the elements have attributes. In those + * cases, a custom {@link org.jivesoftware.smackx.provider.PrivateDataProvider} should be used. + * + * @author Matt Tucker + */ +public class DefaultPrivateData implements PrivateData { + + private String elementName; + private String namespace; + private Map map; + + /** + * Creates a new generic private data object. + * + * @param elementName the name of the element of the XML sub-document. + * @param namespace the namespace of the element. + */ + public DefaultPrivateData(String elementName, String namespace) { + this.elementName = elementName; + this.namespace = namespace; + } + + /** + * Returns the XML element name of the private data sub-packet root element. + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return elementName; + } + + /** + * Returns the XML namespace of the private data sub-packet root element. + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return namespace; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\">"); + for (Iterator i=getNames(); i.hasNext(); ) { + String name = (String)i.next(); + String value = getValue(name); + buf.append("<").append(name).append(">"); + buf.append(value); + buf.append("</").append(name).append(">"); + } + buf.append("</").append(elementName).append(">"); + return buf.toString(); + } + + /** + * Returns an Iterator for the names that can be used to get + * values of the private data. + * + * @return an Iterator for the names. + */ + public synchronized Iterator getNames() { + if (map == null) { + return Collections.EMPTY_LIST.iterator(); + } + return Collections.unmodifiableMap(new HashMap(map)).keySet().iterator(); + } + + /** + * Returns a value given a name. + * + * @param name the name. + * @return the value. + */ + public synchronized String getValue(String name) { + if (map == null) { + return null; + } + return (String)map.get(name); + } + + /** + * Sets a value given the name. + * + * @param name the name. + * @param value the value. + */ + public synchronized void setValue(String name, String value) { + if (map == null) { + map = new HashMap(); + } + map.put(name, value); + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DelayInformation.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DelayInformation.java new file mode 100644 index 000000000..c04409b3a --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DelayInformation.java @@ -0,0 +1,146 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +import org.jivesoftware.smack.packet.PacketExtension; + +/** + * Represents timestamp information about data stored for later delivery. A DelayInformation will + * always includes the timestamp when the packet was originally sent and may include more + * information such as the JID of the entity that originally sent the packet as well as the reason + * for the dealy.<p> + * + * For more information see <a href="http://www.jabber.org/jeps/jep-0091.html">JEP-91</a>. + * + * @author Gaston Dombiak + */ +public class DelayInformation implements PacketExtension { + + public static SimpleDateFormat UTC_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"); + /** + * New date format based on JEP-82 that some clients may use when sending delayed dates. + * JEP-91 is using a SHOULD other servers or clients may be using this format instead of the + * old UTC format. + */ + public static SimpleDateFormat NEW_UTC_FORMAT = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + + static { + UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+0")); + NEW_UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + private Date stamp; + private String from; + private String reason; + + /** + * Creates a new instance with the specified timestamp. + */ + public DelayInformation(Date stamp) { + super(); + this.stamp = stamp; + } + + /** + * Returns the JID of the entity that originally sent the packet or that delayed the + * delivery of the packet or <tt>null</tt> if this information is not available. + * + * @return the JID of the entity that originally sent the packet or that delayed the + * delivery of the packet. + */ + public String getFrom() { + return from; + } + + /** + * Sets the JID of the entity that originally sent the packet or that delayed the + * delivery of the packet or <tt>null</tt> if this information is not available. + * + * @param from the JID of the entity that originally sent the packet. + */ + public void setFrom(String from) { + this.from = from; + } + + /** + * Returns the timstamp when the packet was originally sent. The returned Date is + * be understood as UTC. + * + * @return the timstamp when the packet was originally sent. + */ + public Date getStamp() { + return stamp; + } + + /** + * Returns a natural-language description of the reason for the delay or <tt>null</tt> if + * this information is not available. + * + * @return a natural-language description of the reason for the delay or <tt>null</tt>. + */ + public String getReason() { + return reason; + } + + /** + * Sets a natural-language description of the reason for the delay or <tt>null</tt> if + * this information is not available. + * + * @param reason a natural-language description of the reason for the delay or <tt>null</tt>. + */ + public void setReason(String reason) { + this.reason = reason; + } + + public String getElementName() { + return "x"; + } + + public String getNamespace() { + return "jabber:x:delay"; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\""); + buf.append(" stamp=\""); + synchronized (UTC_FORMAT) { + buf.append(UTC_FORMAT.format(stamp)); + } + buf.append("\""); + if (from != null && from.length() > 0) { + buf.append(" from=\"").append(from).append("\""); + } + buf.append(">"); + if (reason != null && reason.length() > 0) { + buf.append(reason); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverInfo.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverInfo.java new file mode 100644 index 000000000..0694a0ceb --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverInfo.java @@ -0,0 +1,268 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import java.util.*; + +import org.jivesoftware.smack.packet.IQ; + +/** + * A DiscoverInfo IQ packet, which is used by XMPP clients to request and receive information + * to/from other XMPP entities.<p> + * + * The received information may contain one or more identities of the requested XMPP entity, and + * a list of supported features by the requested XMPP entity. + * + * @author Gaston Dombiak + */ +public class DiscoverInfo extends IQ { + + private List features = new ArrayList(); + private List identities = new ArrayList(); + private String node; + + /** + * Adds a new feature to the discovered information. + * + * @param feature the discovered feature + */ + public void addFeature(String feature) { + addFeature(new DiscoverInfo.Feature(feature)); + } + + private void addFeature(Feature feature) { + synchronized (features) { + features.add(feature); + } + } + + /** + * Returns the discovered features of an XMPP entity. + * + * @return an Iterator on the discovered features of an XMPP entity + */ + Iterator getFeatures() { + synchronized (features) { + return Collections.unmodifiableList(new ArrayList(features)).iterator(); + } + } + + /** + * Adds a new identity of the requested entity to the discovered information. + * + * @param identity the discovered entity's identity + */ + public void addIdentity(Identity identity) { + synchronized (identities) { + identities.add(identity); + } + } + + /** + * Returns the discovered identities of an XMPP entity. + * + * @return an Iterator on the discoveted identities + */ + public Iterator getIdentities() { + synchronized (identities) { + return Collections.unmodifiableList(new ArrayList(identities)).iterator(); + } + } + + /** + * Returns the node attribute that supplements the 'jid' attribute. A node is merely + * something that is associated with a JID and for which the JID can provide information.<p> + * + * Node attributes SHOULD be used only when trying to provide or query information which + * is not directly addressable. + * + * @return the node attribute that supplements the 'jid' attribute + */ + public String getNode() { + return node; + } + + /** + * Sets the node attribute that supplements the 'jid' attribute. A node is merely + * something that is associated with a JID and for which the JID can provide information.<p> + * + * Node attributes SHOULD be used only when trying to provide or query information which + * is not directly addressable. + * + * @param node the node attribute that supplements the 'jid' attribute + */ + public void setNode(String node) { + this.node = node; + } + + /** + * Returns true if the specified feature is part of the discovered information. + * + * @param feature the feature to check + * @return true if the requestes feature has been discovered + */ + public boolean containsFeature(String feature) { + for (Iterator it = getFeatures(); it.hasNext();) { + if (feature.equals(((DiscoverInfo.Feature) it.next()).getVar())) + return true; + } + return false; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"http://jabber.org/protocol/disco#info\""); + if (getNode() != null) { + buf.append(" node=\""); + buf.append(getNode()); + buf.append("\""); + } + buf.append(">"); + synchronized (identities) { + for (int i = 0; i < identities.size(); i++) { + Identity identity = (Identity) identities.get(i); + buf.append(identity.toXML()); + } + } + synchronized (features) { + for (int i = 0; i < features.size(); i++) { + Feature feature = (Feature) features.get(i); + buf.append(feature.toXML()); + } + } + // Add packet extensions, if any are defined. + buf.append(getExtensionsXML()); + buf.append("</query>"); + return buf.toString(); + } + + /** + * Represents the identity of a given XMPP entity. An entity may have many identities but all + * the identities SHOULD have the same name.<p> + * + * Refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> + * in order to get the official registry of values for the <i>category</i> and <i>type</i> + * attributes. + * + */ + public static class Identity { + + private String category; + private String name; + private String type; + + /** + * Creates a new identity for an XMPP entity. + * + * @param category the entity's category. + * @param name the entity's name. + */ + public Identity(String category, String name) { + this.category = category; + this.name = name; + } + + /** + * Returns the entity's category. To get the official registry of values for the + * 'category' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> + * + * @return the entity's category. + */ + public String getCategory() { + return category; + } + + /** + * Returns the identity's name. + * + * @return the identity's name. + */ + public String getName() { + return name; + } + + /** + * Returns the entity's type. To get the official registry of values for the + * 'type' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> + * + * @return the entity's type. + */ + public String getType() { + return type; + } + + /** + * Sets the entity's type. To get the official registry of values for the + * 'type' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> + * + * @param type the identity's type. + */ + public void setType(String type) { + this.type = type; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<identity category=\"").append(category).append("\""); + buf.append(" name=\"").append(name).append("\""); + if (type != null) { + buf.append(" type=\"").append(type).append("\""); + } + buf.append("/>"); + return buf.toString(); + } + } + + /** + * Represents the features offered by the item. This information helps requestors determine + * what actions are possible with regard to this item (registration, search, join, etc.) + * as well as specific feature types of interest, if any (e.g., for the purpose of feature + * negotiation). + */ + public static class Feature { + + private String variable; + + /** + * Creates a new feature offered by an XMPP entity or item. + * + * @param variable the feature's variable. + */ + public Feature(String variable) { + this.variable = variable; + } + + /** + * Returns the feature's variable. + * + * @return the feature's variable. + */ + public String getVar() { + return variable; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<feature var=\"").append(variable).append("\"/>"); + return buf.toString(); + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverItems.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverItems.java new file mode 100644 index 000000000..0c264aea9 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/DiscoverItems.java @@ -0,0 +1,235 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import java.util.*; + +import org.jivesoftware.smack.packet.IQ; + +/** + * A DiscoverItems IQ packet, which is used by XMPP clients to request and receive items + * associated with XMPP entities.<p> + * + * The items could also be queried in order to discover if they contain items inside. Some items + * may be addressable by its JID and others may require to be addressed by a JID and a node name. + * + * @author Gaston Dombiak + */ +public class DiscoverItems extends IQ { + + private List items = new ArrayList(); + private String node; + + /** + * Adds a new item to the discovered information. + * + * @param item the discovered entity's item + */ + public void addItem(Item item) { + synchronized (items) { + items.add(item); + } + } + + /** + * Returns the discovered items of the queried XMPP entity. + * + * @return an Iterator on the discovered entity's items + */ + public Iterator getItems() { + synchronized (items) { + return Collections.unmodifiableList(new ArrayList(items)).iterator(); + } + } + + /** + * Returns the node attribute that supplements the 'jid' attribute. A node is merely + * something that is associated with a JID and for which the JID can provide information.<p> + * + * Node attributes SHOULD be used only when trying to provide or query information which + * is not directly addressable. + * + * @return the node attribute that supplements the 'jid' attribute + */ + public String getNode() { + return node; + } + + /** + * Sets the node attribute that supplements the 'jid' attribute. A node is merely + * something that is associated with a JID and for which the JID can provide information.<p> + * + * Node attributes SHOULD be used only when trying to provide or query information which + * is not directly addressable. + * + * @param node the node attribute that supplements the 'jid' attribute + */ + public void setNode(String node) { + this.node = node; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"http://jabber.org/protocol/disco#items\""); + if (getNode() != null) { + buf.append(" node=\""); + buf.append(getNode()); + buf.append("\""); + } + buf.append(">"); + synchronized (items) { + for (int i = 0; i < items.size(); i++) { + Item item = (Item) items.get(i); + buf.append(item.toXML()); + } + } + buf.append("</query>"); + return buf.toString(); + } + + /** + * An item is associated with an XMPP Entity, usually thought of a children of the parent + * entity and normally are addressable as a JID.<p> + * + * An item associated with an entity may not be addressable as a JID. In order to handle + * such items, Service Discovery uses an optional 'node' attribute that supplements the + * 'jid' attribute. + */ + public static class Item { + + /** + * Request to create or update the item. + */ + public static final String UPDATE_ACTION = "update"; + + /** + * Request to remove the item. + */ + public static final String REMOVE_ACTION = "remove"; + + private String entityID; + private String name; + private String node; + private String action; + + /** + * Create a new Item associated with a given entity. + * + * @param entityID the id of the entity that contains the item + */ + public Item(String entityID) { + this.entityID = entityID; + } + + /** + * Returns the entity's ID. + * + * @return the entity's ID. + */ + public String getEntityID() { + return entityID; + } + + /** + * Returns the entity's name. + * + * @return the entity's name. + */ + public String getName() { + return name; + } + + /** + * Sets the entity's name. + * + * @param name the entity's name. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the node attribute that supplements the 'jid' attribute. A node is merely + * something that is associated with a JID and for which the JID can provide information.<p> + * + * Node attributes SHOULD be used only when trying to provide or query information which + * is not directly addressable. + * + * @return the node attribute that supplements the 'jid' attribute + */ + public String getNode() { + return node; + } + + /** + * Sets the node attribute that supplements the 'jid' attribute. A node is merely + * something that is associated with a JID and for which the JID can provide information.<p> + * + * Node attributes SHOULD be used only when trying to provide or query information which + * is not directly addressable. + * + * @param node the node attribute that supplements the 'jid' attribute + */ + public void setNode(String node) { + this.node = node; + } + + /** + * Returns the action that specifies the action being taken for this item. Possible action + * values are: "update" and "remove". Update should either create a new entry if the node + * and jid combination does not already exist, or simply update an existing entry. If + * "remove" is used as the action, the item should be removed from persistent storage. + * + * @return the action being taken for this item + */ + public String getAction() { + return action; + } + + /** + * Sets the action that specifies the action being taken for this item. Possible action + * values are: "update" and "remove". Update should either create a new entry if the node + * and jid combination does not already exist, or simply update an existing entry. If + * "remove" is used as the action, the item should be removed from persistent storage. + * + * @param action the action being taken for this item + */ + public void setAction(String action) { + this.action = action; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<item jid=\"").append(entityID).append("\""); + if (name != null) { + buf.append(" name=\"").append(name).append("\""); + } + if (node != null) { + buf.append(" node=\"").append(node).append("\""); + } + if (action != null) { + buf.append(" action=\"").append(action).append("\""); + } + buf.append("/>"); + return buf.toString(); + } + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/IBBExtensions.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/IBBExtensions.java new file mode 100644 index 000000000..48c1cb150 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/IBBExtensions.java @@ -0,0 +1,241 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.PacketExtension; + +/** + * The different extensions used throughtout the negotiation and transfer + * process. + * + * @author Alexander Wenckus + * + */ +public class IBBExtensions { + + public static final String NAMESPACE = "http://jabber.org/protocol/ibb"; + + private abstract static class IBB extends IQ { + final String sid; + + private IBB(final String sid) { + this.sid = sid; + } + + /** + * Returns the unique stream ID for this file transfer. + * + * @return Returns the unique stream ID for this file transfer. + */ + public String getSessionID() { + return sid; + } + + public String getNamespace() { + return NAMESPACE; + } + } + + /** + * Represents a request to open the file transfer. + * + * @author Alexander Wenckus + * + */ + public static class Open extends IBB { + + public static final String ELEMENT_NAME = "open"; + + private final int blockSize; + + /** + * Constructs an open packet. + * + * @param sid + * The streamID of the file transfer. + * @param blockSize + * The block size of the file transfer. + */ + public Open(final String sid, final int blockSize) { + super(sid); + this.blockSize = blockSize; + } + + /** + * The size blocks in which the data will be sent. + * + * @return The size blocks in which the data will be sent. + */ + public int getBlockSize() { + return blockSize; + } + + public String getElementName() { + return ELEMENT_NAME; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\" "); + buf.append("sid=\"").append(getSessionID()).append("\" "); + buf.append("block-size=\"").append(getBlockSize()).append("\""); + buf.append("/>"); + return buf.toString(); + } + } + + /** + * A data packet containing a portion of the file being sent encoded in + * base64. + * + * @author Alexander Wenckus + * + */ + public static class Data implements PacketExtension { + + private long seq; + + private String data; + + public static final String ELEMENT_NAME = "data"; + + final String sid; + + /** + * Returns the unique stream ID identifying this file transfer. + * + * @return Returns the unique stream ID identifying this file transfer. + */ + public String getSessionID() { + return sid; + } + + public String getNamespace() { + return NAMESPACE; + } + + /** + * A constructor. + * + * @param sid + * The stream ID. + */ + public Data(final String sid) { + this.sid = sid; + } + + public Data(final String sid, final long seq, final String data) { + this(sid); + this.seq = seq; + this.data = data; + } + + public String getElementName() { + return ELEMENT_NAME; + } + + /** + * Returns the data contained in this packet. + * + * @return Returns the data contained in this packet. + */ + public String getData() { + return data; + } + + /** + * Sets the data contained in this packet. + * + * @param data + * The data encoded in base65 + */ + public void setData(final String data) { + this.data = data; + } + + /** + * Returns the sequence of this packet in regard to the other data + * packets. + * + * @return Returns the sequence of this packet in regard to the other + * data packets. + */ + public long getSeq() { + return seq; + } + + /** + * Sets the sequence of this packet. + * + * @param seq + * A number between 0 and 65535 + */ + public void setSeq(final long seq) { + this.seq = seq; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()) + .append("\" "); + buf.append("sid=\"").append(getSessionID()).append("\" "); + buf.append("seq=\"").append(getSeq()).append("\""); + buf.append(">"); + buf.append(getData()); + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + } + + /** + * Represents the closing of the file transfer. + * + * + * @author Alexander Wenckus + * + */ + public static class Close extends IBB { + public static final String ELEMENT_NAME = "close"; + + /** + * The constructor. + * + * @param sid + * The unique stream ID identifying this file transfer. + */ + public Close(String sid) { + super(sid); + } + + public String getElementName() { + return ELEMENT_NAME; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\" "); + buf.append("sid=\"").append(getSessionID()).append("\""); + buf.append("/>"); + return buf.toString(); + } + + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/LastActivity.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/LastActivity.java new file mode 100644 index 000000000..7145784e8 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/LastActivity.java @@ -0,0 +1,158 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.xmlpull.v1.XmlPullParser; + +/** + * A last activity IQ for retrieving information about the last activity associated with a Jabber ID. + * LastActivity (JEP-012) allows for retrieval of how long a particular user has been idle and the + * message the specified when doing so. + * To get the last activity of a user, simple send the LastActivity packet to them, as in the + * following code example: + * <p/> + * <pre> + * XMPPConnection con = new XMPPConnection("jabber.org"); + * con.login("john", "doe"); + * LastActivity activity = LastActivity.getLastActivity(con, "xray@jabber.org"); + * </pre> + * + * @author Derek DeMoro + */ +public class LastActivity extends IQ { + + public long lastActivity; + public String message; + + public LastActivity() { + setType(IQ.Type.GET); + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"jabber:iq:last\"></query>"); + return buf.toString(); + } + + + private void setLastActivity(long lastActivity) { + this.lastActivity = lastActivity; + } + + + private void setMessage(String message) { + this.message = message; + } + + /** + * Returns number of seconds that have passed since the user last logged out. + * If the user is offline, 0 will be returned. + * + * @return the number of seconds that have passed since the user last logged out. + */ + public long getIdleTime() { + return lastActivity; + } + + + /** + * Returns the status message of the last unavailable presence received from the user. + * + * @return the status message of the last unavailable presence received from the user + */ + public String getStatusMessage() { + return message; + } + + + /** + * The IQ Provider for LastActivity. + * + * @author Derek DeMoro + */ + public static class Provider implements IQProvider { + + public Provider() { + super(); + } + + public IQ parseIQ(XmlPullParser parser) throws Exception { + if (parser.getEventType() != XmlPullParser.START_TAG) { + throw new IllegalStateException("Parser not in proper position, or bad XML."); + } + + LastActivity lastActivity = new LastActivity(); + try { + String seconds = parser.getAttributeValue("", "seconds"); + String message = parser.nextText(); + if (seconds != null) { + long xmlSeconds = new Double(seconds).longValue(); + lastActivity.setLastActivity((int)xmlSeconds); + } + + if (message != null) { + lastActivity.setMessage(message); + } + } + catch (Exception e) { + e.printStackTrace(); + } + return lastActivity; + } + } + + /** + * Retrieve the last activity of a particular jid. + * @param con the current XMPPConnection. + * @param jid the JID of the user. + * @return the LastActivity packet of the jid. + * @throws XMPPException thrown if a server error has occured. + */ + public static LastActivity getLastActivity(XMPPConnection con, String jid) throws XMPPException { + LastActivity activity = new LastActivity(); + jid = StringUtils.parseBareAddress(jid); + activity.setTo(jid); + + PacketCollector collector = con.createPacketCollector(new PacketIDFilter(activity.getPacketID())); + con.sendPacket(activity); + + LastActivity response = (LastActivity) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Cancel the collector. + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from server on status set."); + } + if (response.getError() != null) { + throw new XMPPException(response.getError()); + } + return response; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCAdmin.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCAdmin.java new file mode 100644 index 000000000..f1e877296 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCAdmin.java @@ -0,0 +1,234 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; +import java.util.*; + +import org.jivesoftware.smack.packet.IQ; + +/** + * IQ packet that serves for kicking users, granting and revoking voice, banning users, + * modifying the ban list, granting and revoking membership and granting and revoking + * moderator privileges. All these operations are scoped by the + * 'http://jabber.org/protocol/muc#admin' namespace. + * + * @author Gaston Dombiak + */ +public class MUCAdmin extends IQ { + + private List items = new ArrayList(); + + /** + * Returns an Iterator for item childs that holds information about roles, affiliation, + * jids and nicks. + * + * @return an Iterator for item childs that holds information about roles, affiliation, + * jids and nicks. + */ + public Iterator getItems() { + synchronized (items) { + return Collections.unmodifiableList(new ArrayList(items)).iterator(); + } + } + + /** + * Adds an item child that holds information about roles, affiliation, jids and nicks. + * + * @param item the item child that holds information about roles, affiliation, jids and nicks. + */ + public void addItem(Item item) { + synchronized (items) { + items.add(item); + } + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"http://jabber.org/protocol/muc#admin\">"); + synchronized (items) { + for (int i = 0; i < items.size(); i++) { + Item item = (Item) items.get(i); + buf.append(item.toXML()); + } + } + // Add packet extensions, if any are defined. + buf.append(getExtensionsXML()); + buf.append("</query>"); + return buf.toString(); + } + + /** + * Item child that holds information about roles, affiliation, jids and nicks. + * + * @author Gaston Dombiak + */ + public static class Item { + private String actor; + private String reason; + private String affiliation; + private String jid; + private String nick; + private String role; + + /** + * Creates a new item child. + * + * @param affiliation the actor's affiliation to the room + * @param role the privilege level of an occupant within a room. + */ + public Item(String affiliation, String role) { + this.affiliation = affiliation; + this.role = role; + } + + /** + * Returns the actor (JID of an occupant in the room) that was kicked or banned. + * + * @return the JID of an occupant in the room that was kicked or banned. + */ + public String getActor() { + return actor; + } + + /** + * Returns the reason for the item child. The reason is optional and could be used to + * explain the reason why a user (occupant) was kicked or banned. + * + * @return the reason for the item child. + */ + public String getReason() { + return reason; + } + + /** + * Returns the occupant's affiliation to the room. The affiliation is a semi-permanent + * association or connection with a room. The possible affiliations are "owner", "admin", + * "member", and "outcast" (naturally it is also possible to have no affiliation). An + * affiliation lasts across a user's visits to a room. + * + * @return the actor's affiliation to the room + */ + public String getAffiliation() { + return affiliation; + } + + /** + * Returns the <room@service/nick> by which an occupant is identified within the context + * of a room. If the room is non-anonymous, the JID will be included in the item. + * + * @return the room JID by which an occupant is identified within the room. + */ + public String getJid() { + return jid; + } + + /** + * Returns the new nickname of an occupant that is changing his/her nickname. The new + * nickname is sent as part of the unavailable presence. + * + * @return the new nickname of an occupant that is changing his/her nickname. + */ + public String getNick() { + return nick; + } + + /** + * Returns the temporary position or privilege level of an occupant within a room. The + * possible roles are "moderator", "participant", and "visitor" (it is also possible to + * have no defined role). A role lasts only for the duration of an occupant's visit to + * a room. + * + * @return the privilege level of an occupant within a room. + */ + public String getRole() { + return role; + } + + /** + * Sets the actor (JID of an occupant in the room) that was kicked or banned. + * + * @param actor the actor (JID of an occupant in the room) that was kicked or banned. + */ + public void setActor(String actor) { + this.actor = actor; + } + + /** + * Sets the reason for the item child. The reason is optional and could be used to + * explain the reason why a user (occupant) was kicked or banned. + * + * @param reason the reason why a user (occupant) was kicked or banned. + */ + public void setReason(String reason) { + this.reason = reason; + } + + /** + * Sets the <room@service/nick> by which an occupant is identified within the context + * of a room. If the room is non-anonymous, the JID will be included in the item. + * + * @param jid the JID by which an occupant is identified within a room. + */ + public void setJid(String jid) { + this.jid = jid; + } + + /** + * Sets the new nickname of an occupant that is changing his/her nickname. The new + * nickname is sent as part of the unavailable presence. + * + * @param nick the new nickname of an occupant that is changing his/her nickname. + */ + public void setNick(String nick) { + this.nick = nick; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<item"); + if (getAffiliation() != null) { + buf.append(" affiliation=\"").append(getAffiliation()).append("\""); + } + if (getJid() != null) { + buf.append(" jid=\"").append(getJid()).append("\""); + } + if (getNick() != null) { + buf.append(" nick=\"").append(getNick()).append("\""); + } + if (getRole() != null) { + buf.append(" role=\"").append(getRole()).append("\""); + } + if (getReason() == null && getActor() == null) { + buf.append("/>"); + } + else { + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + if (getActor() != null) { + buf.append("<actor jid=\"").append(getActor()).append("\"/>"); + } + buf.append("</item>"); + } + return buf.toString(); + } + }; +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCInitialPresence.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCInitialPresence.java new file mode 100644 index 000000000..88ba7f29b --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCInitialPresence.java @@ -0,0 +1,223 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +import org.jivesoftware.smack.packet.PacketExtension; + +/** + * Represents extended presence information whose sole purpose is to signal the ability of + * the occupant to speak the MUC protocol when joining a room. If the room requires a password + * then the MUCInitialPresence should include one.<p> + * + * The amount of discussion history provided on entering a room (perhaps because the + * user is on a low-bandwidth connection or is using a small-footprint client) could be managed by + * setting a configured History instance to the MUCInitialPresence instance. + * @see MUCInitialPresence#setHistory(MUCInitialPresence.History). + * + * @author Gaston Dombiak + */ +public class MUCInitialPresence implements PacketExtension { + + private String password; + private History history; + + public String getElementName() { + return "x"; + } + + public String getNamespace() { + return "http://jabber.org/protocol/muc"; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\">"); + if (getPassword() != null) { + buf.append("<password>").append(getPassword()).append("</password>"); + } + if (getHistory() != null) { + buf.append(getHistory().toXML()); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + /** + * Returns the history that manages the amount of discussion history provided on + * entering a room. + * + * @return the history that manages the amount of discussion history provided on + * entering a room. + */ + public History getHistory() { + return history; + } + + /** + * Returns the password to use when the room requires a password. + * + * @return the password to use when the room requires a password. + */ + public String getPassword() { + return password; + } + + /** + * Sets the History that manages the amount of discussion history provided on + * entering a room. + * + * @param history that manages the amount of discussion history provided on + * entering a room. + */ + public void setHistory(History history) { + this.history = history; + } + + /** + * Sets the password to use when the room requires a password. + * + * @param password the password to use when the room requires a password. + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * The History class controls the number of characters or messages to receive + * when entering a room. + * + * @author Gaston Dombiak + */ + public static class History { + + private int maxChars = -1; + private int maxStanzas = -1; + private int seconds = -1; + private Date since; + + /** + * Returns the total number of characters to receive in the history. + * + * @return total number of characters to receive in the history. + */ + public int getMaxChars() { + return maxChars; + } + + /** + * Returns the total number of messages to receive in the history. + * + * @return the total number of messages to receive in the history. + */ + public int getMaxStanzas() { + return maxStanzas; + } + + /** + * Returns the number of seconds to use to filter the messages received during that time. + * In other words, only the messages received in the last "X" seconds will be included in + * the history. + * + * @return the number of seconds to use to filter the messages received during that time. + */ + public int getSeconds() { + return seconds; + } + + /** + * Returns the since date to use to filter the messages received during that time. + * In other words, only the messages received since the datetime specified will be + * included in the history. + * + * @return the since date to use to filter the messages received during that time. + */ + public Date getSince() { + return since; + } + + /** + * Sets the total number of characters to receive in the history. + * + * @param maxChars the total number of characters to receive in the history. + */ + public void setMaxChars(int maxChars) { + this.maxChars = maxChars; + } + + /** + * Sets the total number of messages to receive in the history. + * + * @param maxStanzas the total number of messages to receive in the history. + */ + public void setMaxStanzas(int maxStanzas) { + this.maxStanzas = maxStanzas; + } + + /** + * Sets the number of seconds to use to filter the messages received during that time. + * In other words, only the messages received in the last "X" seconds will be included in + * the history. + * + * @param seconds the number of seconds to use to filter the messages received during + * that time. + */ + public void setSeconds(int seconds) { + this.seconds = seconds; + } + + /** + * Sets the since date to use to filter the messages received during that time. + * In other words, only the messages received since the datetime specified will be + * included in the history. + * + * @param since the since date to use to filter the messages received during that time. + */ + public void setSince(Date since) { + this.since = since; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<history"); + if (getMaxChars() != -1) { + buf.append(" maxchars=\"").append(getMaxChars()).append("\""); + } + if (getMaxStanzas() != -1) { + buf.append(" maxstanzas=\"").append(getMaxStanzas()).append("\""); + } + if (getSeconds() != -1) { + buf.append(" seconds=\"").append(getSeconds()).append("\""); + } + if (getSince() != null) { + SimpleDateFormat utcFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + utcFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + buf.append(" since=\"").append(utcFormat.format(getSince())).append("\""); + } + buf.append("/>"); + return buf.toString(); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCOwner.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCOwner.java new file mode 100644 index 000000000..626746884 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCOwner.java @@ -0,0 +1,339 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; +import java.util.*; + +import org.jivesoftware.smack.packet.IQ; + +/** + * IQ packet that serves for granting and revoking ownership privileges, granting + * and revoking administrative privileges and destroying a room. All these operations + * are scoped by the 'http://jabber.org/protocol/muc#owner' namespace. + * + * @author Gaston Dombiak + */ +public class MUCOwner extends IQ { + + private List items = new ArrayList(); + private Destroy destroy; + + /** + * Returns an Iterator for item childs that holds information about affiliation, + * jids and nicks. + * + * @return an Iterator for item childs that holds information about affiliation, + * jids and nicks. + */ + public Iterator getItems() { + synchronized (items) { + return Collections.unmodifiableList(new ArrayList(items)).iterator(); + } + } + + /** + * Returns a request to the server to destroy a room. The sender of the request + * should be the room's owner. If the sender of the destroy request is not the room's owner + * then the server will answer a "Forbidden" error. + * + * @return a request to the server to destroy a room. + */ + public Destroy getDestroy() { + return destroy; + } + + /** + * Sets a request to the server to destroy a room. The sender of the request + * should be the room's owner. If the sender of the destroy request is not the room's owner + * then the server will answer a "Forbidden" error. + * + * @param destroy the request to the server to destroy a room. + */ + public void setDestroy(Destroy destroy) { + this.destroy = destroy; + } + + /** + * Adds an item child that holds information about affiliation, jids and nicks. + * + * @param item the item child that holds information about affiliation, jids and nicks. + */ + public void addItem(Item item) { + synchronized (items) { + items.add(item); + } + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"http://jabber.org/protocol/muc#owner\">"); + synchronized (items) { + for (int i = 0; i < items.size(); i++) { + Item item = (Item) items.get(i); + buf.append(item.toXML()); + } + } + if (getDestroy() != null) { + buf.append(getDestroy().toXML()); + } + // Add packet extensions, if any are defined. + buf.append(getExtensionsXML()); + buf.append("</query>"); + return buf.toString(); + } + + /** + * Item child that holds information about affiliation, jids and nicks. + * + * @author Gaston Dombiak + */ + public static class Item { + + private String actor; + private String reason; + private String affiliation; + private String jid; + private String nick; + private String role; + + /** + * Creates a new item child. + * + * @param affiliation the actor's affiliation to the room + */ + public Item(String affiliation) { + this.affiliation = affiliation; + } + + /** + * Returns the actor (JID of an occupant in the room) that was kicked or banned. + * + * @return the JID of an occupant in the room that was kicked or banned. + */ + public String getActor() { + return actor; + } + + /** + * Returns the reason for the item child. The reason is optional and could be used to + * explain the reason why a user (occupant) was kicked or banned. + * + * @return the reason for the item child. + */ + public String getReason() { + return reason; + } + + /** + * Returns the occupant's affiliation to the room. The affiliation is a semi-permanent + * association or connection with a room. The possible affiliations are "owner", "admin", + * "member", and "outcast" (naturally it is also possible to have no affiliation). An + * affiliation lasts across a user's visits to a room. + * + * @return the actor's affiliation to the room + */ + public String getAffiliation() { + return affiliation; + } + + /** + * Returns the <room@service/nick> by which an occupant is identified within the context + * of a room. If the room is non-anonymous, the JID will be included in the item. + * + * @return the room JID by which an occupant is identified within the room. + */ + public String getJid() { + return jid; + } + + /** + * Returns the new nickname of an occupant that is changing his/her nickname. The new + * nickname is sent as part of the unavailable presence. + * + * @return the new nickname of an occupant that is changing his/her nickname. + */ + public String getNick() { + return nick; + } + + /** + * Returns the temporary position or privilege level of an occupant within a room. The + * possible roles are "moderator", "participant", and "visitor" (it is also possible to + * have no defined role). A role lasts only for the duration of an occupant's visit to + * a room. + * + * @return the privilege level of an occupant within a room. + */ + public String getRole() { + return role; + } + + /** + * Sets the actor (JID of an occupant in the room) that was kicked or banned. + * + * @param actor the actor (JID of an occupant in the room) that was kicked or banned. + */ + public void setActor(String actor) { + this.actor = actor; + } + + /** + * Sets the reason for the item child. The reason is optional and could be used to + * explain the reason why a user (occupant) was kicked or banned. + * + * @param reason the reason why a user (occupant) was kicked or banned. + */ + public void setReason(String reason) { + this.reason = reason; + } + + /** + * Sets the <room@service/nick> by which an occupant is identified within the context + * of a room. If the room is non-anonymous, the JID will be included in the item. + * + * @param jid the JID by which an occupant is identified within a room. + */ + public void setJid(String jid) { + this.jid = jid; + } + + /** + * Sets the new nickname of an occupant that is changing his/her nickname. The new + * nickname is sent as part of the unavailable presence. + * + * @param nick the new nickname of an occupant that is changing his/her nickname. + */ + public void setNick(String nick) { + this.nick = nick; + } + + /** + * Sets the temporary position or privilege level of an occupant within a room. The + * possible roles are "moderator", "participant", and "visitor" (it is also possible to + * have no defined role). A role lasts only for the duration of an occupant's visit to + * a room. + * + * @param role the new privilege level of an occupant within a room. + */ + public void setRole(String role) { + this.role = role; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<item"); + if (getAffiliation() != null) { + buf.append(" affiliation=\"").append(getAffiliation()).append("\""); + } + if (getJid() != null) { + buf.append(" jid=\"").append(getJid()).append("\""); + } + if (getNick() != null) { + buf.append(" nick=\"").append(getNick()).append("\""); + } + if (getRole() != null) { + buf.append(" role=\"").append(getRole()).append("\""); + } + if (getReason() == null && getActor() == null) { + buf.append("/>"); + } + else { + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + if (getActor() != null) { + buf.append("<actor jid=\"").append(getActor()).append("\"/>"); + } + buf.append("</item>"); + } + return buf.toString(); + } + }; + + /** + * Represents a request to the server to destroy a room. The sender of the request + * should be the room's owner. If the sender of the destroy request is not the room's owner + * then the server will answer a "Forbidden" error. + * + * @author Gaston Dombiak + */ + public static class Destroy { + private String reason; + private String jid; + + + /** + * Returns the JID of an alternate location since the current room is being destroyed. + * + * @return the JID of an alternate location. + */ + public String getJid() { + return jid; + } + + /** + * Returns the reason for the room destruction. + * + * @return the reason for the room destruction. + */ + public String getReason() { + return reason; + } + + /** + * Sets the JID of an alternate location since the current room is being destroyed. + * + * @param jid the JID of an alternate location. + */ + public void setJid(String jid) { + this.jid = jid; + } + + /** + * Sets the reason for the room destruction. + * + * @param reason the reason for the room destruction. + */ + public void setReason(String reason) { + this.reason = reason; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<destroy"); + if (getJid() != null) { + buf.append(" jid=\"").append(getJid()).append("\""); + } + if (getReason() == null) { + buf.append("/>"); + } + else { + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + buf.append("</destroy>"); + } + return buf.toString(); + } + + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCUser.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCUser.java new file mode 100644 index 000000000..7e84cea6b --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MUCUser.java @@ -0,0 +1,627 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; + +/** + * Represents extended presence information about roles, affiliations, full JIDs, + * or status codes scoped by the 'http://jabber.org/protocol/muc#user' namespace. + * + * @author Gaston Dombiak + */ +public class MUCUser implements PacketExtension { + + private Invite invite; + private Decline decline; + private Item item; + private String password; + private Status status; + private Destroy destroy; + + public String getElementName() { + return "x"; + } + + public String getNamespace() { + return "http://jabber.org/protocol/muc#user"; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\">"); + if (getInvite() != null) { + buf.append(getInvite().toXML()); + } + if (getDecline() != null) { + buf.append(getDecline().toXML()); + } + if (getItem() != null) { + buf.append(getItem().toXML()); + } + if (getPassword() != null) { + buf.append("<password>").append(getPassword()).append("</password>"); + } + if (getStatus() != null) { + buf.append(getStatus().toXML()); + } + if (getDestroy() != null) { + buf.append(getDestroy().toXML()); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + /** + * Returns the invitation for another user to a room. The sender of the invitation + * must be an occupant of the room. The invitation will be sent to the room which in turn + * will forward the invitation to the invitee. + * + * @return an invitation for another user to a room. + */ + public Invite getInvite() { + return invite; + } + + /** + * Returns the rejection to an invitation from another user to a room. The rejection will be + * sent to the room which in turn will forward the refusal to the inviter. + * + * @return a rejection to an invitation from another user to a room. + */ + public Decline getDecline() { + return decline; + } + + /** + * Returns the item child that holds information about roles, affiliation, jids and nicks. + * + * @return an item child that holds information about roles, affiliation, jids and nicks. + */ + public Item getItem() { + return item; + } + + /** + * Returns the password to use to enter Password-Protected Room. A Password-Protected Room is + * a room that a user cannot enter without first providing the correct password. + * + * @return the password to use to enter Password-Protected Room. + */ + public String getPassword() { + return password; + } + + /** + * Returns the status which holds a code that assists in presenting notification messages. + * + * @return the status which holds a code that assists in presenting notification messages. + */ + public Status getStatus() { + return status; + } + + /** + * Returns the notification that the room has been destroyed. After a room has been destroyed, + * the room occupants will receive a Presence packet of type 'unavailable' with the reason for + * the room destruction if provided by the room owner. + * + * @return a notification that the room has been destroyed. + */ + public Destroy getDestroy() { + return destroy; + } + + /** + * Sets the invitation for another user to a room. The sender of the invitation + * must be an occupant of the room. The invitation will be sent to the room which in turn + * will forward the invitation to the invitee. + * + * @param invite the invitation for another user to a room. + */ + public void setInvite(Invite invite) { + this.invite = invite; + } + + /** + * Sets the rejection to an invitation from another user to a room. The rejection will be + * sent to the room which in turn will forward the refusal to the inviter. + * + * @param decline the rejection to an invitation from another user to a room. + */ + public void setDecline(Decline decline) { + this.decline = decline; + } + + /** + * Sets the item child that holds information about roles, affiliation, jids and nicks. + * + * @param item the item child that holds information about roles, affiliation, jids and nicks. + */ + public void setItem(Item item) { + this.item = item; + } + + /** + * Sets the password to use to enter Password-Protected Room. A Password-Protected Room is + * a room that a user cannot enter without first providing the correct password. + * + * @param string the password to use to enter Password-Protected Room. + */ + public void setPassword(String string) { + password = string; + } + + /** + * Sets the status which holds a code that assists in presenting notification messages. + * + * @param status the status which holds a code that assists in presenting notification + * messages. + */ + public void setStatus(Status status) { + this.status = status; + } + + /** + * Sets the notification that the room has been destroyed. After a room has been destroyed, + * the room occupants will receive a Presence packet of type 'unavailable' with the reason for + * the room destruction if provided by the room owner. + * + * @param destroy the notification that the room has been destroyed. + */ + public void setDestroy(Destroy destroy) { + this.destroy = destroy; + } + + /** + * Represents an invitation for another user to a room. The sender of the invitation + * must be an occupant of the room. The invitation will be sent to the room which in turn + * will forward the invitation to the invitee. + * + * @author Gaston Dombiak + */ + public static class Invite { + private String reason; + private String from; + private String to; + + /** + * Returns the bare JID of the inviter or, optionally, the room JID. (e.g. + * 'crone1@shakespeare.lit/desktop'). + * + * @return the room's occupant that sent the invitation. + */ + public String getFrom() { + return from; + } + + /** + * Returns the message explaining the invitation. + * + * @return the message explaining the invitation. + */ + public String getReason() { + return reason; + } + + /** + * Returns the bare JID of the invitee. (e.g. 'hecate@shakespeare.lit') + * + * @return the bare JID of the invitee. + */ + public String getTo() { + return to; + } + + /** + * Sets the bare JID of the inviter or, optionally, the room JID. (e.g. + * 'crone1@shakespeare.lit/desktop') + * + * @param from the bare JID of the inviter or, optionally, the room JID. + */ + public void setFrom(String from) { + this.from = from; + } + + /** + * Sets the message explaining the invitation. + * + * @param reason the message explaining the invitation. + */ + public void setReason(String reason) { + this.reason = reason; + } + + /** + * Sets the bare JID of the invitee. (e.g. 'hecate@shakespeare.lit') + * + * @param to the bare JID of the invitee. + */ + public void setTo(String to) { + this.to = to; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<invite "); + if (getTo() != null) { + buf.append(" to=\"").append(getTo()).append("\""); + } + if (getFrom() != null) { + buf.append(" from=\"").append(getFrom()).append("\""); + } + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + buf.append("</invite>"); + return buf.toString(); + } + }; + + /** + * Represents a rejection to an invitation from another user to a room. The rejection will be + * sent to the room which in turn will forward the refusal to the inviter. + * + * @author Gaston Dombiak + */ + public static class Decline { + private String reason; + private String from; + private String to; + + /** + * Returns the bare JID of the invitee that rejected the invitation. (e.g. + * 'crone1@shakespeare.lit/desktop'). + * + * @return the bare JID of the invitee that rejected the invitation. + */ + public String getFrom() { + return from; + } + + /** + * Returns the message explaining why the invitation was rejected. + * + * @return the message explaining the reason for the rejection. + */ + public String getReason() { + return reason; + } + + /** + * Returns the bare JID of the inviter. (e.g. 'hecate@shakespeare.lit') + * + * @return the bare JID of the inviter. + */ + public String getTo() { + return to; + } + + /** + * Sets the bare JID of the invitee that rejected the invitation. (e.g. + * 'crone1@shakespeare.lit/desktop'). + * + * @param from the bare JID of the invitee that rejected the invitation. + */ + public void setFrom(String from) { + this.from = from; + } + + /** + * Sets the message explaining why the invitation was rejected. + * + * @param reason the message explaining the reason for the rejection. + */ + public void setReason(String reason) { + this.reason = reason; + } + + /** + * Sets the bare JID of the inviter. (e.g. 'hecate@shakespeare.lit') + * + * @param to the bare JID of the inviter. + */ + public void setTo(String to) { + this.to = to; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<decline "); + if (getTo() != null) { + buf.append(" to=\"").append(getTo()).append("\""); + } + if (getFrom() != null) { + buf.append(" from=\"").append(getFrom()).append("\""); + } + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + buf.append("</decline>"); + return buf.toString(); + } + }; + + /** + * Item child that holds information about roles, affiliation, jids and nicks. + * + * @author Gaston Dombiak + */ + public static class Item { + private String actor; + private String reason; + private String affiliation; + private String jid; + private String nick; + private String role; + + /** + * Creates a new item child. + * + * @param affiliation the actor's affiliation to the room + * @param role the privilege level of an occupant within a room. + */ + public Item(String affiliation, String role) { + this.affiliation = affiliation; + this.role = role; + } + + /** + * Returns the actor (JID of an occupant in the room) that was kicked or banned. + * + * @return the JID of an occupant in the room that was kicked or banned. + */ + public String getActor() { + return actor == null ? "" : actor; + } + + /** + * Returns the reason for the item child. The reason is optional and could be used to + * explain the reason why a user (occupant) was kicked or banned. + * + * @return the reason for the item child. + */ + public String getReason() { + return reason == null ? "" : reason; + } + + /** + * Returns the occupant's affiliation to the room. The affiliation is a semi-permanent + * association or connection with a room. The possible affiliations are "owner", "admin", + * "member", and "outcast" (naturally it is also possible to have no affiliation). An + * affiliation lasts across a user's visits to a room. + * + * @return the actor's affiliation to the room + */ + public String getAffiliation() { + return affiliation; + } + + /** + * Returns the <room@service/nick> by which an occupant is identified within the context + * of a room. If the room is non-anonymous, the JID will be included in the item. + * + * @return the room JID by which an occupant is identified within the room. + */ + public String getJid() { + return jid; + } + + /** + * Returns the new nickname of an occupant that is changing his/her nickname. The new + * nickname is sent as part of the unavailable presence. + * + * @return the new nickname of an occupant that is changing his/her nickname. + */ + public String getNick() { + return nick; + } + + /** + * Returns the temporary position or privilege level of an occupant within a room. The + * possible roles are "moderator", "participant", and "visitor" (it is also possible to + * have no defined role). A role lasts only for the duration of an occupant's visit to + * a room. + * + * @return the privilege level of an occupant within a room. + */ + public String getRole() { + return role; + } + + /** + * Sets the actor (JID of an occupant in the room) that was kicked or banned. + * + * @param actor the actor (JID of an occupant in the room) that was kicked or banned. + */ + public void setActor(String actor) { + this.actor = actor; + } + + /** + * Sets the reason for the item child. The reason is optional and could be used to + * explain the reason why a user (occupant) was kicked or banned. + * + * @param reason the reason why a user (occupant) was kicked or banned. + */ + public void setReason(String reason) { + this.reason = reason; + } + + /** + * Sets the <room@service/nick> by which an occupant is identified within the context + * of a room. If the room is non-anonymous, the JID will be included in the item. + * + * @param jid the JID by which an occupant is identified within a room. + */ + public void setJid(String jid) { + this.jid = jid; + } + + /** + * Sets the new nickname of an occupant that is changing his/her nickname. The new + * nickname is sent as part of the unavailable presence. + * + * @param nick the new nickname of an occupant that is changing his/her nickname. + */ + public void setNick(String nick) { + this.nick = nick; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<item"); + if (getAffiliation() != null) { + buf.append(" affiliation=\"").append(getAffiliation()).append("\""); + } + if (getJid() != null) { + buf.append(" jid=\"").append(getJid()).append("\""); + } + if (getNick() != null) { + buf.append(" nick=\"").append(getNick()).append("\""); + } + if (getRole() != null) { + buf.append(" role=\"").append(getRole()).append("\""); + } + if (getReason() == null && getActor() == null) { + buf.append("/>"); + } + else { + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + if (getActor() != null) { + buf.append("<actor jid=\"").append(getActor()).append("\"/>"); + } + buf.append("</item>"); + } + return buf.toString(); + } + }; + + /** + * Status code assists in presenting notification messages. The following link provides the + * list of existing error codes (@link http://www.jabber.org/jeps/jep-0045.html#errorstatus). + * + * @author Gaston Dombiak + */ + public static class Status { + private String code; + + /** + * Creates a new instance of Status with the specified code. + * + * @param code the code that uniquely identifies the reason of the error. + */ + public Status(String code) { + this.code = code; + } + + /** + * Returns the code that uniquely identifies the reason of the error. The code + * assists in presenting notification messages. + * + * @return the code that uniquely identifies the reason of the error. + */ + public String getCode() { + return code; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<status code=\"").append(getCode()).append("\"/>"); + return buf.toString(); + } + }; + + /** + * Represents a notification that the room has been destroyed. After a room has been destroyed, + * the room occupants will receive a Presence packet of type 'unavailable' with the reason for + * the room destruction if provided by the room owner. + * + * @author Gaston Dombiak + */ + public static class Destroy { + private String reason; + private String jid; + + + /** + * Returns the JID of an alternate location since the current room is being destroyed. + * + * @return the JID of an alternate location. + */ + public String getJid() { + return jid; + } + + /** + * Returns the reason for the room destruction. + * + * @return the reason for the room destruction. + */ + public String getReason() { + return reason; + } + + /** + * Sets the JID of an alternate location since the current room is being destroyed. + * + * @param jid the JID of an alternate location. + */ + public void setJid(String jid) { + this.jid = jid; + } + + /** + * Sets the reason for the room destruction. + * + * @param reason the reason for the room destruction. + */ + public void setReason(String reason) { + this.reason = reason; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<destroy"); + if (getJid() != null) { + buf.append(" jid=\"").append(getJid()).append("\""); + } + if (getReason() == null) { + buf.append("/>"); + } + else { + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + buf.append("</destroy>"); + } + return buf.toString(); + } + + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MessageEvent.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MessageEvent.java new file mode 100644 index 000000000..617e9d14e --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MessageEvent.java @@ -0,0 +1,334 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import java.util.*; + +import org.jivesoftware.smack.packet.PacketExtension; + +/** + * Represents message events relating to the delivery, display, composition and cancellation of + * messages.<p> + * + * There are four message events currently defined in this namespace: + * <ol> + * <li>Offline<br> + * Indicates that the message has been stored offline by the intended recipient's server. This + * event is triggered only if the intended recipient's server supports offline storage, has that + * support enabled, and the recipient is offline when the server receives the message for delivery.</li> + * + * <li>Delivered<br> + * Indicates that the message has been delivered to the recipient. This signifies that the message + * has reached the recipient's XMPP client, but does not necessarily mean that the message has + * been displayed. This event is to be raised by the XMPP client.</li> + * + * <li>Displayed<br> + * Once the message has been received by the recipient's XMPP client, it may be displayed to the + * user. This event indicates that the message has been displayed, and is to be raised by the + * XMPP client. Even if a message is displayed multiple times, this event should be raised only + * once.</li> + * + * <li>Composing<br> + * In threaded chat conversations, this indicates that the recipient is composing a reply to a + * message. The event is to be raised by the recipient's XMPP client. A XMPP client is allowed + * to raise this event multiple times in response to the same request, providing the original + * event is cancelled first.</li> + * </ol> + * + * @author Gaston Dombiak + */ +public class MessageEvent implements PacketExtension { + + public static final String OFFLINE = "offline"; + public static final String COMPOSING = "composing"; + public static final String DISPLAYED = "displayed"; + public static final String DELIVERED = "delivered"; + public static final String CANCELLED = "cancelled"; + + private boolean offline = false; + private boolean delivered = false; + private boolean displayed = false; + private boolean composing = false; + private boolean cancelled = true; + + private String packetID = null; + + /** + * Returns the XML element name of the extension sub-packet root element. + * Always returns "x" + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return "x"; + } + + /** + * Returns the XML namespace of the extension sub-packet root element. + * According the specification the namespace is always "jabber:x:event" + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return "jabber:x:event"; + } + + /** + * When the message is a request returns if the sender of the message requests to be notified + * when the receiver is composing a reply. + * When the message is a notification returns if the receiver of the message is composing a + * reply. + * + * @return true if the sender is requesting to be notified when composing or when notifying + * that the receiver of the message is composing a reply + */ + public boolean isComposing() { + return composing; + } + + /** + * When the message is a request returns if the sender of the message requests to be notified + * when the message is delivered. + * When the message is a notification returns if the message was delivered or not. + * + * @return true if the sender is requesting to be notified when delivered or when notifying + * that the message was delivered + */ + public boolean isDelivered() { + return delivered; + } + + /** + * When the message is a request returns if the sender of the message requests to be notified + * when the message is displayed. + * When the message is a notification returns if the message was displayed or not. + * + * @return true if the sender is requesting to be notified when displayed or when notifying + * that the message was displayed + */ + public boolean isDisplayed() { + return displayed; + } + + /** + * When the message is a request returns if the sender of the message requests to be notified + * when the receiver of the message is offline. + * When the message is a notification returns if the receiver of the message was offline. + * + * @return true if the sender is requesting to be notified when offline or when notifying + * that the receiver of the message is offline + */ + public boolean isOffline() { + return offline; + } + + /** + * When the message is a notification returns if the receiver of the message cancelled + * composing a reply. + * + * @return true if the receiver of the message cancelled composing a reply + */ + public boolean isCancelled() { + return cancelled; + } + + /** + * Returns the unique ID of the message that requested to be notified of the event. + * The packet id is not used when the message is a request for notifications + * + * @return the message id that requested to be notified of the event. + */ + public String getPacketID() { + return packetID; + } + + /** + * Returns the types of events. The type of event could be: + * "offline", "composing","delivered","displayed", "offline" + * + * @return an iterator over all the types of events of the MessageEvent. + */ + public Iterator getEventTypes() { + ArrayList allEvents = new ArrayList(); + if (isDelivered()) { + allEvents.add(MessageEvent.DELIVERED); + } + if (!isMessageEventRequest() && isCancelled()) { + allEvents.add(MessageEvent.CANCELLED); + } + if (isComposing()) { + allEvents.add(MessageEvent.COMPOSING); + } + if (isDisplayed()) { + allEvents.add(MessageEvent.DISPLAYED); + } + if (isOffline()) { + allEvents.add(MessageEvent.OFFLINE); + } + return allEvents.iterator(); + } + + /** + * When the message is a request sets if the sender of the message requests to be notified + * when the receiver is composing a reply. + * When the message is a notification sets if the receiver of the message is composing a + * reply. + * + * @param composing sets if the sender is requesting to be notified when composing or when + * notifying that the receiver of the message is composing a reply + */ + public void setComposing(boolean composing) { + this.composing = composing; + setCancelled(false); + } + + /** + * When the message is a request sets if the sender of the message requests to be notified + * when the message is delivered. + * When the message is a notification sets if the message was delivered or not. + * + * @param delivered sets if the sender is requesting to be notified when delivered or when + * notifying that the message was delivered + */ + public void setDelivered(boolean delivered) { + this.delivered = delivered; + setCancelled(false); + } + + /** + * When the message is a request sets if the sender of the message requests to be notified + * when the message is displayed. + * When the message is a notification sets if the message was displayed or not. + * + * @param displayed sets if the sender is requesting to be notified when displayed or when + * notifying that the message was displayed + */ + public void setDisplayed(boolean displayed) { + this.displayed = displayed; + setCancelled(false); + } + + /** + * When the message is a request sets if the sender of the message requests to be notified + * when the receiver of the message is offline. + * When the message is a notification sets if the receiver of the message was offline. + * + * @param offline sets if the sender is requesting to be notified when offline or when + * notifying that the receiver of the message is offline + */ + public void setOffline(boolean offline) { + this.offline = offline; + setCancelled(false); + } + + /** + * When the message is a notification sets if the receiver of the message cancelled + * composing a reply. + * The Cancelled event is never requested explicitly. It is requested implicitly when + * requesting to be notified of the Composing event. + * + * @param cancelled sets if the receiver of the message cancelled composing a reply + */ + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + /** + * Sets the unique ID of the message that requested to be notified of the event. + * The packet id is not used when the message is a request for notifications + * + * @param packetID the message id that requested to be notified of the event. + */ + public void setPacketID(String packetID) { + this.packetID = packetID; + } + + /** + * Returns true if this MessageEvent is a request for notifications. + * Returns false if this MessageEvent is a notification of an event. + * + * @return true if this message is a request for notifications. + */ + public boolean isMessageEventRequest() { + return this.packetID == null; + } + + /** + * Returns the XML representation of a Message Event according the specification. + * + * Usually the XML representation will be inside of a Message XML representation like + * in the following examples:<p> + * + * Request to be notified when displayed: + * <pre> + * <message + * to='romeo@montague.net/orchard' + * from='juliet@capulet.com/balcony' + * id='message22'> + * <x xmlns='jabber:x:event'> + * <displayed/> + * </x> + * </message> + * </pre> + * + * Notification of displayed: + * <pre> + * <message + * from='romeo@montague.net/orchard' + * to='juliet@capulet.com/balcony'> + * <x xmlns='jabber:x:event'> + * <displayed/> + * <id>message22</id> + * </x> + * </message> + * </pre> + * + */ + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\">"); + // Note: Cancellation events don't specify any tag. They just send the packetID + + // Add the offline tag if the sender requests to be notified of offline events or if + // the target is offline + if (isOffline()) + buf.append("<").append(MessageEvent.OFFLINE).append("/>"); + // Add the delivered tag if the sender requests to be notified when the message is + // delivered or if the target notifies that the message has been delivered + if (isDelivered()) + buf.append("<").append(MessageEvent.DELIVERED).append("/>"); + // Add the displayed tag if the sender requests to be notified when the message is + // displayed or if the target notifies that the message has been displayed + if (isDisplayed()) + buf.append("<").append(MessageEvent.DISPLAYED).append("/>"); + // Add the composing tag if the sender requests to be notified when the target is + // composing a reply or if the target notifies that he/she is composing a reply + if (isComposing()) + buf.append("<").append(MessageEvent.COMPOSING).append("/>"); + // Add the id tag only if the MessageEvent is a notification message (not a request) + if (getPacketID() != null) + buf.append("<id>").append(getPacketID()).append("</id>"); + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MultipleAddresses.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MultipleAddresses.java new file mode 100644 index 000000000..1222fc3cd --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/MultipleAddresses.java @@ -0,0 +1,205 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Packet extension that contains the list of addresses that a packet should be sent or was sent. + * + * @author Gaston Dombiak + */ +public class MultipleAddresses implements PacketExtension { + + public static final String BCC = "bcc"; + public static final String CC = "cc"; + public static final String NO_REPLY = "noreply"; + public static final String REPLY_ROOM = "replyroom"; + public static final String REPLY_TO = "replyto"; + public static final String TO = "to"; + + + private List addresses = new ArrayList(); + + /** + * Adds a new address to which the packet is going to be sent or was sent. + * + * @param type on of the static type (BCC, CC, NO_REPLY, REPLY_ROOM, etc.) + * @param jid the JID address of the recipient. + * @param node used to specify a sub-addressable unit at a particular JID, corresponding to + * a Service Discovery node. + * @param desc used to specify human-readable information for this address. + * @param delivered true when the packet was already delivered to this address. + * @param uri used to specify an external system address, such as a sip:, sips:, or im: URI. + */ + public void addAddress(String type, String jid, String node, String desc, boolean delivered, + String uri) { + // Create a new address with the specificed configuration + Address address = new Address(type); + address.setJid(jid); + address.setNode(node); + address.setDescription(desc); + address.setDelivered(delivered); + address.setUri(uri); + // Add the new address to the list of multiple recipients + addresses.add(address); + } + + /** + * Indicate that the packet being sent should not be replied. + */ + public void setNoReply() { + // Create a new address with the specificed configuration + Address address = new Address(NO_REPLY); + // Add the new address to the list of multiple recipients + addresses.add(address); + } + + /** + * Returns the list of addresses that matches the specified type. Examples of address + * type are: TO, CC, BCC, etc.. + * + * @param type Examples of address type are: TO, CC, BCC, etc. + * @return the list of addresses that matches the specified type. + */ + public List getAddressesOfType(String type) { + List answer = new ArrayList(addresses.size()); + for (Iterator it = addresses.iterator(); it.hasNext();) { + Address address = (Address) it.next(); + if (address.getType().equals(type)) { + answer.add(address); + } + } + + return answer; + } + + public String getElementName() { + return "addresses"; + } + + public String getNamespace() { + return "http://jabber.org/protocol/address"; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()); + buf.append(" xmlns=\"").append(getNamespace()).append("\">"); + // Loop through all the addresses and append them to the string buffer + for (Iterator i = addresses.iterator(); i.hasNext();) { + Address address = (Address) i.next(); + buf.append(address.toXML()); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + public static class Address { + + private String type; + private String jid; + private String node; + private String description; + private boolean delivered; + private String uri; + + private Address(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public String getJid() { + return jid; + } + + private void setJid(String jid) { + this.jid = jid; + } + + public String getNode() { + return node; + } + + private void setNode(String node) { + this.node = node; + } + + public String getDescription() { + return description; + } + + private void setDescription(String description) { + this.description = description; + } + + public boolean isDelivered() { + return delivered; + } + + private void setDelivered(boolean delivered) { + this.delivered = delivered; + } + + public String getUri() { + return uri; + } + + private void setUri(String uri) { + this.uri = uri; + } + + private String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<address type=\""); + // Append the address type (e.g. TO/CC/BCC) + buf.append(type).append("\""); + if (jid != null) { + buf.append(" jid=\""); + buf.append(jid).append("\""); + } + if (node != null) { + buf.append(" node=\""); + buf.append(node).append("\""); + } + if (description != null && description.trim().length() > 0) { + buf.append(" desc=\""); + buf.append(description).append("\""); + } + if (delivered) { + buf.append(" delivered=\"true\""); + } + if (uri != null) { + buf.append(" uri=\""); + buf.append(uri).append("\""); + } + buf.append("/>"); + return buf.toString(); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageInfo.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageInfo.java new file mode 100644 index 000000000..90c1ead71 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageInfo.java @@ -0,0 +1,128 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.xmlpull.v1.XmlPullParser; + +/** + * OfflineMessageInfo is an extension included in the retrieved offline messages requested by + * the {@link org.jivesoftware.smackx.OfflineMessageManager}. This extension includes a stamp + * that uniquely identifies the offline message. This stamp may be used for deleting the offline + * message. The stamp may be of the form UTC timestamps but it is not required to have that format. + * + * @author Gaston Dombiak + */ +public class OfflineMessageInfo implements PacketExtension { + + private String node = null; + + /** + * Returns the XML element name of the extension sub-packet root element. + * Always returns "offline" + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return "offline"; + } + + /** + * Returns the XML namespace of the extension sub-packet root element. + * According the specification the namespace is always "http://jabber.org/protocol/offline" + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return "http://jabber.org/protocol/offline"; + } + + /** + * Returns the stamp that uniquely identifies the offline message. This stamp may + * be used for deleting the offline message. The stamp may be of the form UTC timestamps + * but it is not required to have that format. + * + * @return the stamp that uniquely identifies the offline message. + */ + public String getNode() { + return node; + } + + /** + * Sets the stamp that uniquely identifies the offline message. This stamp may + * be used for deleting the offline message. The stamp may be of the form UTC timestamps + * but it is not required to have that format. + * + * @param node the stamp that uniquely identifies the offline message. + */ + public void setNode(String node) { + this.node = node; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\">"); + if (getNode() != null) + buf.append("<item node=\"").append(getNode()).append("\"/>"); + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + public static class Provider implements PacketExtensionProvider { + + /** + * Creates a new Provider. + * ProviderManager requires that every PacketExtensionProvider has a public, + * no-argument constructor + */ + public Provider() { + } + + /** + * Parses a OfflineMessageInfo packet (extension sub-packet). + * + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public PacketExtension parseExtension(XmlPullParser parser) + throws Exception { + OfflineMessageInfo info = new OfflineMessageInfo(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) + info.setNode(parser.getAttributeValue("", "node")); + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("offline")) { + done = true; + } + } + } + + return info; + } + + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageRequest.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageRequest.java new file mode 100644 index 000000000..9b726e3c6 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/OfflineMessageRequest.java @@ -0,0 +1,237 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.xmlpull.v1.XmlPullParser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Represents a request to get some or all the offline messages of a user. This class can also + * be used for deleting some or all the offline messages of a user. + * + * @author Gaston Dombiak + */ +public class OfflineMessageRequest extends IQ { + + private List items = new ArrayList(); + private boolean purge = false; + private boolean fetch = false; + + /** + * Returns an Iterator for item childs that holds information about offline messages to + * view or delete. + * + * @return an Iterator for item childs that holds information about offline messages to + * view or delete. + */ + public Iterator getItems() { + synchronized (items) { + return Collections.unmodifiableList(new ArrayList(items)).iterator(); + } + } + + /** + * Adds an item child that holds information about offline messages to view or delete. + * + * @param item the item child that holds information about offline messages to view or delete. + */ + public void addItem(Item item) { + synchronized (items) { + items.add(item); + } + } + + /** + * Returns true if all the offline messages of the user should be deleted. + * + * @return true if all the offline messages of the user should be deleted. + */ + public boolean isPurge() { + return purge; + } + + /** + * Sets if all the offline messages of the user should be deleted. + * + * @param purge true if all the offline messages of the user should be deleted. + */ + public void setPurge(boolean purge) { + this.purge = purge; + } + + /** + * Returns true if all the offline messages of the user should be retrieved. + * + * @return true if all the offline messages of the user should be retrieved. + */ + public boolean isFetch() { + return fetch; + } + + /** + * Sets if all the offline messages of the user should be retrieved. + * + * @param fetch true if all the offline messages of the user should be retrieved. + */ + public void setFetch(boolean fetch) { + this.fetch = fetch; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<offline xmlns=\"http://jabber.org/protocol/offline\">"); + synchronized (items) { + for (int i = 0; i < items.size(); i++) { + Item item = (Item) items.get(i); + buf.append(item.toXML()); + } + } + if (purge) { + buf.append("<purge/>"); + } + if (fetch) { + buf.append("<fetch/>"); + } + // Add packet extensions, if any are defined. + buf.append(getExtensionsXML()); + buf.append("</offline>"); + return buf.toString(); + } + + /** + * Item child that holds information about offline messages to view or delete. + * + * @author Gaston Dombiak + */ + public static class Item { + private String action; + private String jid; + private String node; + + /** + * Creates a new item child. + * + * @param node the actor's affiliation to the room + */ + public Item(String node) { + this.node = node; + } + + public String getNode() { + return node; + } + + /** + * Returns "view" or "remove" that indicate if the server should return the specified + * offline message or delete it. + * + * @return "view" or "remove" that indicate if the server should return the specified + * offline message or delete it. + */ + public String getAction() { + return action; + } + + /** + * Sets if the server should return the specified offline message or delete it. Possible + * values are "view" or "remove". + * + * @param action if the server should return the specified offline message or delete it. + */ + public void setAction(String action) { + this.action = action; + } + + public String getJid() { + return jid; + } + + public void setJid(String jid) { + this.jid = jid; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<item"); + if (getAction() != null) { + buf.append(" action=\"").append(getAction()).append("\""); + } + if (getJid() != null) { + buf.append(" jid=\"").append(getJid()).append("\""); + } + if (getNode() != null) { + buf.append(" node=\"").append(getNode()).append("\""); + } + buf.append("/>"); + return buf.toString(); + } + } + + public static class Provider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + OfflineMessageRequest request = new OfflineMessageRequest(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + request.addItem(parseItem(parser)); + } + else if (parser.getName().equals("purge")) { + request.setPurge(true); + } + else if (parser.getName().equals("fetch")) { + request.setFetch(true); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("offline")) { + done = true; + } + } + } + + return request; + } + + private Item parseItem(XmlPullParser parser) throws Exception { + boolean done = false; + Item item = new Item(parser.getAttributeValue("", "node")); + item.setAction(parser.getAttributeValue("", "action")); + item.setJid(parser.getAttributeValue("", "jid")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + done = true; + } + } + } + return item; + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/PrivateData.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/PrivateData.java new file mode 100644 index 000000000..c83269c51 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/PrivateData.java @@ -0,0 +1,52 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +/** + * Interface to represent private data. Each private data chunk is an XML sub-document + * with a root element name and namespace. + * + * @see org.jivesoftware.smackx.PrivateDataManager + * @author Matt Tucker + */ +public interface PrivateData { + + /** + * Returns the root element name. + * + * @return the element name. + */ + public String getElementName(); + + /** + * Returns the root element XML namespace. + * + * @return the namespace. + */ + public String getNamespace(); + + /** + * Returns the XML reppresentation of the PrivateData. + * + * @return the private data as XML. + */ + public String toXML(); +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/RosterExchange.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/RosterExchange.java new file mode 100644 index 000000000..553c6dcdf --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/RosterExchange.java @@ -0,0 +1,175 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import java.util.*; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.*; + +/** + * Represents XMPP Roster Item Exchange packets.<p> + * + * The 'jabber:x:roster' namespace (which is not to be confused with the 'jabber:iq:roster' + * namespace) is used to send roster items from one client to another. A roster item is sent by + * adding to the <message/> element an <x/> child scoped by the 'jabber:x:roster' namespace. This + * <x/> element may contain one or more <item/> children (one for each roster item to be sent).<p> + * + * Each <item/> element may possess the following attributes:<p> + * + * <jid/> -- The id of the contact being sent. This attribute is required.<br> + * <name/> -- A natural-language nickname for the contact. This attribute is optional.<p> + * + * Each <item/> element may also contain one or more <group/> children specifying the + * natural-language name of a user-specified group, for the purpose of categorizing this contact + * into one or more roster groups. + * + * @author Gaston Dombiak + */ +public class RosterExchange implements PacketExtension { + + private List remoteRosterEntries = new ArrayList(); + + /** + * Creates a new empty roster exchange package. + * + */ + public RosterExchange() { + super(); + } + + /** + * Creates a new roster exchange package with the entries specified in roster. + * + * @param roster the roster to send to other XMPP entity. + */ + public RosterExchange(Roster roster) { + // Add all the roster entries to the new RosterExchange + for (Iterator rosterEntries = roster.getEntries(); rosterEntries.hasNext();) { + this.addRosterEntry((RosterEntry) rosterEntries.next()); + } + } + + /** + * Adds a roster entry to the packet. + * + * @param rosterEntry a roster entry to add. + */ + public void addRosterEntry(RosterEntry rosterEntry) { + // Obtain a String[] from the roster entry groups name + ArrayList groupNamesList = new ArrayList(); + String[] groupNames; + for (Iterator groups = rosterEntry.getGroups(); groups.hasNext();) { + groupNamesList.add(((RosterGroup) groups.next()).getName()); + } + groupNames = (String[]) groupNamesList.toArray(new String[groupNamesList.size()]); + + // Create a new Entry based on the rosterEntry and add it to the packet + RemoteRosterEntry remoteRosterEntry = new RemoteRosterEntry(rosterEntry.getUser(), rosterEntry.getName(), groupNames); + + addRosterEntry(remoteRosterEntry); + } + + /** + * Adds a remote roster entry to the packet. + * + * @param remoteRosterEntry a remote roster entry to add. + */ + public void addRosterEntry(RemoteRosterEntry remoteRosterEntry) { + synchronized (remoteRosterEntries) { + remoteRosterEntries.add(remoteRosterEntry); + } + } + + /** + * Returns the XML element name of the extension sub-packet root element. + * Always returns "x" + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return "x"; + } + + /** + * Returns the XML namespace of the extension sub-packet root element. + * According the specification the namespace is always "jabber:x:roster" + * (which is not to be confused with the 'jabber:iq:roster' namespace + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return "jabber:x:roster"; + } + + /** + * Returns an Iterator for the roster entries in the packet. + * + * @return an Iterator for the roster entries in the packet. + */ + public Iterator getRosterEntries() { + synchronized (remoteRosterEntries) { + List entries = Collections.unmodifiableList(new ArrayList(remoteRosterEntries)); + return entries.iterator(); + } + } + + /** + * Returns a count of the entries in the roster exchange. + * + * @return the number of entries in the roster exchange. + */ + public int getEntryCount() { + return remoteRosterEntries.size(); + } + + /** + * Returns the XML representation of a Roster Item Exchange according the specification. + * + * Usually the XML representation will be inside of a Message XML representation like + * in the following example: + * <pre> + * <message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"> + * <subject>Any subject you want</subject> + * <body>This message contains roster items.</body> + * <x xmlns="jabber:x:roster"> + * <item jid="gato1@gato.home"/> + * <item jid="gato2@gato.home"/> + * </x> + * </message> + * </pre> + * + */ + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\">"); + // Loop through all roster entries and append them to the string buffer + for (Iterator i = getRosterEntries(); i.hasNext();) { + RemoteRosterEntry remoteRosterEntry = (RemoteRosterEntry) i.next(); + buf.append(remoteRosterEntry.toXML()); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/SharedGroupsInfo.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/SharedGroupsInfo.java new file mode 100644 index 000000000..b572a7ae6 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/SharedGroupsInfo.java @@ -0,0 +1,73 @@ +package org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.xmlpull.v1.XmlPullParser; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * IQ packet used for discovering the user's shared groups and for getting the answer back + * from the server.<p> + * + * Important note: This functionality is not part of the XMPP spec and it will only work + * with Wildfire. + * + * @author Gaston Dombiak + */ +public class SharedGroupsInfo extends IQ { + + private List groups = new ArrayList(); + + /** + * Returns a collection with the shared group names returned from the server. + * + * @return collection with the shared group names returned from the server. + */ + public List getGroups() { + return groups; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<sharedgroup xmlns=\"http://www.jivesoftware.org/protocol/sharedgroup\">"); + for (Iterator it=groups.iterator(); it.hasNext();) { + buf.append("<group>").append(it.next()).append("</group>"); + } + buf.append("</sharedgroup>"); + return buf.toString(); + } + + /** + * Internal Search service Provider. + */ + public static class Provider implements IQProvider { + + /** + * Provider Constructor. + */ + public Provider() { + super(); + } + + public IQ parseIQ(XmlPullParser parser) throws Exception { + SharedGroupsInfo groupsInfo = new SharedGroupsInfo(); + + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG && parser.getName().equals("group")) { + groupsInfo.getGroups().add(parser.nextText()); + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("sharedgroup")) { + done = true; + } + } + } + return groupsInfo; + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/StreamInitiation.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/StreamInitiation.java new file mode 100644 index 000000000..75f44814c --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/StreamInitiation.java @@ -0,0 +1,419 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.util.StringUtils; + +import java.util.Date; + +/** + * The process by which two entities initiate a stream. + * + * @author Alexander Wenckus + */ +public class StreamInitiation extends IQ { + + private String id; + + private String mimeType; + + private File file; + + private Feature featureNegotiation; + + /** + * The "id" attribute is an opaque identifier. This attribute MUST be + * present on type='set', and MUST be a valid string. This SHOULD NOT be + * sent back on type='result', since the <iq/> "id" attribute provides the + * only context needed. This value is generated by the Sender, and the same + * value MUST be used throughout a session when talking to the Receiver. + * + * @param id The "id" attribute. + */ + public void setSesssionID(final String id) { + this.id = id; + } + + /** + * Uniquely identifies a stream initiation to the recipient. + * + * @return The "id" attribute. + * @see #setSesssionID(String) + */ + public String getSessionID() { + return id; + } + + /** + * The "mime-type" attribute identifies the MIME-type for the data across + * the stream. This attribute MUST be a valid MIME-type as registered with + * the Internet Assigned Numbers Authority (IANA) [3] (specifically, as + * listed at <http://www.iana.org/assignments/media-types>). During + * negotiation, this attribute SHOULD be present, and is otherwise not + * required. If not included during negotiation, its value is assumed to be + * "binary/octect-stream". + * + * @param mimeType The valid mime-type. + */ + public void setMimeType(final String mimeType) { + this.mimeType = mimeType; + } + + /** + * Identifies the type of file that is desired to be transfered. + * + * @return The mime-type. + * @see #setMimeType(String) + */ + public String getMimeType() { + return mimeType; + } + + /** + * Sets the file which contains the information pertaining to the file to be + * transfered. + * + * @param file The file identified by the stream initiator to be sent. + */ + public void setFile(final File file) { + this.file = file; + } + + /** + * Returns the file containing the information about the request. + * + * @return Returns the file containing the information about the request. + */ + public File getFile() { + return file; + } + + /** + * Sets the data form which contains the valid methods of stream neotiation + * and transfer. + * + * @param form The dataform containing the methods. + */ + public void setFeatureNegotiationForm(final DataForm form) { + this.featureNegotiation = new Feature(form); + } + + /** + * Returns the data form which contains the valid methods of stream + * neotiation and transfer. + * + * @return Returns the data form which contains the valid methods of stream + * neotiation and transfer. + */ + public DataForm getFeatureNegotiationForm() { + return featureNegotiation.getData(); + } + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smack.packet.IQ#getChildElementXML() + */ + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + if (this.getType().equals(IQ.Type.SET)) { + buf.append("<si xmlns=\"http://jabber.org/protocol/si\" "); + if (getSessionID() != null) { + buf.append("id=\"").append(getSessionID()).append("\" "); + } + if (getMimeType() != null) { + buf.append("mime-type=\"").append(getMimeType()).append("\" "); + } + buf + .append("profile=\"http://jabber.org/protocol/si/profile/file-transfer\">"); + + // Add the file section if there is one. + String fileXML = file.toXML(); + if (fileXML != null) { + buf.append(fileXML); + } + } + else if (this.getType().equals(IQ.Type.RESULT)) { + buf.append("<si xmlns=\"http://jabber.org/protocol/si\">"); + } + else { + throw new IllegalArgumentException("IQ Type not understood"); + } + if (featureNegotiation != null) { + buf.append(featureNegotiation.toXML()); + } + buf.append("</si>"); + return buf.toString(); + } + + /** + * <ul> + * <li>size: The size, in bytes, of the data to be sent.</li> + * <li>name: The name of the file that the Sender wishes to send.</li> + * <li>date: The last modification time of the file. This is specified + * using the DateTime profile as described in Jabber Date and Time Profiles.</li> + * <li>hash: The MD5 sum of the file contents.</li> + * </ul> + * <p/> + * <p/> + * <desc> is used to provide a sender-generated description of the + * file so the receiver can better understand what is being sent. It MUST + * NOT be sent in the result. + * <p/> + * <p/> + * When <range> is sent in the offer, it should have no attributes. + * This signifies that the sender can do ranged transfers. When a Stream + * Initiation result is sent with the <range> element, it uses these + * attributes: + * <p/> + * <ul> + * <li>offset: Specifies the position, in bytes, to start transferring the + * file data from. This defaults to zero (0) if not specified.</li> + * <li>length - Specifies the number of bytes to retrieve starting at + * offset. This defaults to the length of the file from offset to the end.</li> + * </ul> + * <p/> + * <p/> + * Both attributes are OPTIONAL on the <range> element. Sending no + * attributes is synonymous with not sending the <range> element. When + * no <range> element is sent in the Stream Initiation result, the + * Sender MUST send the complete file starting at offset 0. More generally, + * data is sent over the stream byte for byte starting at the offset + * position for the length specified. + * + * @author Alexander Wenckus + */ + public static class File implements PacketExtension { + + private final String name; + + private final long size; + + private String hash; + + private Date date; + + private String desc; + + private boolean isRanged; + + /** + * Constructor providing the name of the file and its size. + * + * @param name The name of the file. + * @param size The size of the file in bytes. + */ + public File(final String name, final long size) { + if (name == null) { + throw new NullPointerException("name cannot be null"); + } + + this.name = name; + this.size = size; + } + + /** + * Returns the file's name. + * + * @return Returns the file's name. + */ + public String getName() { + return name; + } + + /** + * Returns the file's size. + * + * @return Returns the file's size. + */ + public long getSize() { + return size; + } + + /** + * Sets the MD5 sum of the file's contents + * + * @param hash The MD5 sum of the file's contents. + */ + public void setHash(final String hash) { + this.hash = hash; + } + + /** + * Returns the MD5 sum of the file's contents + * + * @return Returns the MD5 sum of the file's contents + */ + public String getHash() { + return hash; + } + + /** + * Sets the date that the file was last modified. + * + * @param date The date that the file was last modified. + */ + public void setDate(Date date) { + this.date = date; + } + + /** + * Returns the date that the file was last modified. + * + * @return Returns the date that the file was last modified. + */ + public Date getDate() { + return date; + } + + /** + * Sets the description of the file. + * + * @param desc The description of the file so that the file reciever can + * know what file it is. + */ + public void setDesc(final String desc) { + this.desc = desc; + } + + /** + * Returns the description of the file. + * + * @return Returns the description of the file. + */ + public String getDesc() { + return desc; + } + + /** + * True if a range can be provided and false if it cannot. + * + * @param isRanged True if a range can be provided and false if it cannot. + */ + public void setRanged(final boolean isRanged) { + this.isRanged = isRanged; + } + + /** + * Returns whether or not the initiator can support a range for the file + * tranfer. + * + * @return Returns whether or not the initiator can support a range for + * the file tranfer. + */ + public boolean isRanged() { + return isRanged; + } + + public String getElementName() { + return "file"; + } + + public String getNamespace() { + return "http://jabber.org/protocol/si/profile/file-transfer"; + } + + public String toXML() { + StringBuffer buffer = new StringBuffer(); + + buffer.append("<").append(getElementName()).append(" xmlns=\"") + .append(getNamespace()).append("\" "); + + if (getName() != null) { + buffer.append("name=\"").append(getName()).append("\" "); + } + + if (getSize() > 0) { + buffer.append("size=\"").append(getSize()).append("\" "); + } + + if (getDate() != null) { + buffer.append("date=\"").append(DelayInformation.UTC_FORMAT.format(date)).append("\" "); + } + + if (getHash() != null) { + buffer.append("hash=\"").append(getHash()).append("\" "); + } + + if ((desc != null && desc.length() > 0) || isRanged) { + buffer.append(">"); + if (getDesc() != null && desc.length() > 0) { + buffer.append("<desc>").append(StringUtils.escapeForXML(getDesc())).append("</desc>"); + } + if (isRanged()) { + buffer.append("<range/>"); + } + buffer.append("</").append(getElementName()).append(">"); + } + else { + buffer.append("/>"); + } + return buffer.toString(); + } + } + + /** + * The feature negotiation portion of the StreamInitiation packet. + * + * @author Alexander Wenckus + * + */ + public class Feature implements PacketExtension { + + private final DataForm data; + + /** + * The dataform can be provided as part of the constructor. + * + * @param data The dataform. + */ + public Feature(final DataForm data) { + this.data = data; + } + + /** + * Returns the dataform associated with the feature negotiation. + * + * @return Returns the dataform associated with the feature negotiation. + */ + public DataForm getData() { + return data; + } + + public String getNamespace() { + return "http://jabber.org/protocol/feature-neg"; + } + + public String getElementName() { + return "feature"; + } + + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf + .append("<feature xmlns=\"http://jabber.org/protocol/feature-neg\">"); + buf.append(data.toXML()); + buf.append("</feature>"); + return buf.toString(); + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Time.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Time.java new file mode 100644 index 000000000..9ceea620f --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Time.java @@ -0,0 +1,196 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; + +import java.util.*; +import java.text.SimpleDateFormat; +import java.text.DateFormat; + +/** + * A Time IQ packet, which is used by XMPP clients to exchange their respective local + * times. Clients that wish to fully support the entitity time protocol should register + * a PacketListener for incoming time requests that then respond with the local time. + * This class can be used to request the time from other clients, such as in the + * following code snippet: + * + * <pre> + * // Request the time from a remote user. + * Time timeRequest = new Time(); + * timeRequest.setType(IQ.Type.GET); + * timeRequest.setTo(someUser@example.com); + * + * // Create a packet collector to listen for a response. + * PacketCollector collector = con.createPacketCollector( + * new PacketIDFilter(timeRequest.getPacketID())); + * + * con.sendPacket(timeRequest); + * + * // Wait up to 5 seconds for a result. + * IQ result = (IQ)collector.nextResult(5000); + * if (result != null && result.getType() == IQ.Type.RESULT) { + * Time timeResult = (Time)result; + * // Do something with result... + * }</pre><p> + * + * Warning: this is an non-standard protocol documented by + * <a href="http://www.jabber.org/jeps/jep-0090.html">JEP-90</a>. Because this is a + * non-standard protocol, it is subject to change. + * + * @author Matt Tucker + */ +public class Time extends IQ { + + private static SimpleDateFormat utcFormat = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"); + private static DateFormat displayFormat = DateFormat.getDateTimeInstance(); + + private String utc = null; + private String tz = null; + private String display = null; + + /** + * Creates a new Time instance with empty values for all fields. + */ + public Time() { + this(Calendar.getInstance()); + } + + /** + * Creates a new Time instance using the specified calendar instance as + * the time value to send. + * + * @param cal the time value. + */ + public Time(Calendar cal) { + TimeZone timeZone = cal.getTimeZone(); + tz = cal.getTimeZone().getID(); + display = displayFormat.format(cal.getTime()); + // Convert local time to the UTC time. + utc = utcFormat.format(new Date( + cal.getTimeInMillis() - timeZone.getOffset(cal.getTimeInMillis()))); + } + + /** + * Returns the local time or <tt>null</tt> if the time hasn't been set. + * + * @return the lcocal time. + */ + public Date getTime() { + if (utc == null) { + return null; + } + Date date = null; + try { + Calendar cal = Calendar.getInstance(); + // Convert the UTC time to local time. + cal.setTime(new Date(utcFormat.parse(utc).getTime() + + cal.getTimeZone().getOffset(cal.getTimeInMillis()))); + date = cal.getTime(); + } + catch (Exception e) { + e.printStackTrace(); + } + return date; + } + + /** + * Sets the time using the local time. + * + * @param time the current local time. + */ + public void setTime(Date time) { + // Convert local time to UTC time. + utc = utcFormat.format(new Date( + time.getTime() - TimeZone.getDefault().getOffset(time.getTime()))); + } + + /** + * Returns the time as a UTC formatted String using the format CCYYMMDDThh:mm:ss. + * + * @return the time as a UTC formatted String. + */ + public String getUtc() { + return utc; + } + + /** + * Sets the time using UTC formatted String in the format CCYYMMDDThh:mm:ss. + * + * @param utc the time using a formatted String. + */ + public void setUtc(String utc) { + this.utc = utc; + + } + + /** + * Returns the time zone. + * + * @return the time zone. + */ + public String getTz() { + return tz; + } + + /** + * Sets the time zone. + * + * @param tz the time zone. + */ + public void setTz(String tz) { + this.tz = tz; + } + + /** + * Returns the local (non-utc) time in human-friendly format. + * + * @return the local time in human-friendly format. + */ + public String getDisplay() { + return display; + } + + /** + * Sets the local time in human-friendly format. + * + * @param display the local time in human-friendly format. + */ + public void setDisplay(String display) { + this.display = display; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"jabber:iq:time\">"); + if (utc != null) { + buf.append("<utc>").append(utc).append("</utc>"); + } + if (tz != null) { + buf.append("<tz>").append(tz).append("</tz>"); + } + if (display != null) { + buf.append("<display>").append(display).append("</display>"); + } + buf.append("</query>"); + return buf.toString(); + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/VCard.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/VCard.java new file mode 100644 index 000000000..4f1248859 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/VCard.java @@ -0,0 +1,798 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.util.StringUtils; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * A VCard class for use with the + * <a href="http://www.jivesoftware.org/smack/" target="_blank">SMACK jabber library</a>.<p> + * <p/> + * You should refer to the + * <a href="http://www.jabber.org/jeps/jep-0054.html" target="_blank">JEP-54 documentation</a>.<p> + * <p/> + * Please note that this class is incomplete but it does provide the most commonly found + * information in vCards. Also remember that VCard transfer is not a standard, and the protocol + * may change or be replaced.<p> + * <p/> + * <b>Usage:</b> + * <pre> + * <p/> + * // To save VCard: + * <p/> + * VCard vCard = new VCard(); + * vCard.setFirstName("kir"); + * vCard.setLastName("max"); + * vCard.setEmailHome("foo@fee.bar"); + * vCard.setJabberId("jabber@id.org"); + * vCard.setOrganization("Jetbrains, s.r.o"); + * vCard.setNickName("KIR"); + * <p/> + * vCard.setField("TITLE", "Mr"); + * vCard.setAddressFieldHome("STREET", "Some street"); + * vCard.setAddressFieldWork("CTRY", "US"); + * vCard.setPhoneWork("FAX", "3443233"); + * <p/> + * vCard.save(connection); + * <p/> + * // To load VCard: + * <p/> + * VCard vCard = new VCard(); + * vCard.load(conn); // load own VCard + * vCard.load(conn, "joe@foo.bar"); // load someone's VCard + * </pre> + * + * @author Kirill Maximov (kir@maxkir.com) + */ +public class VCard extends IQ { + + /** + * Phone types: + * VOICE?, FAX?, PAGER?, MSG?, CELL?, VIDEO?, BBS?, MODEM?, ISDN?, PCS?, PREF? + */ + private Map homePhones = new HashMap(); + private Map workPhones = new HashMap(); + + + /** + * Address types: + * POSTAL?, PARCEL?, (DOM | INTL)?, PREF?, POBOX?, EXTADR?, STREET?, LOCALITY?, + * REGION?, PCODE?, CTRY? + */ + private Map homeAddr = new HashMap(); + private Map workAddr = new HashMap(); + + private String firstName; + private String lastName; + private String middleName; + + private String emailHome; + private String emailWork; + + private String organization; + private String organizationUnit; + + private String avatar; + + /** + * Such as DESC ROLE GEO etc.. see JEP-0054 + */ + private Map otherSimpleFields = new HashMap(); + + public VCard() { + } + + /** + * Set generic VCard field. + * + * @param field value of field. Possible values: NICKNAME, PHOTO, BDAY, JABBERID, MAILER, TZ, + * GEO, TITLE, ROLE, LOGO, NOTE, PRODID, REV, SORT-STRING, SOUND, UID, URL, DESC. + */ + public String getField(String field) { + return (String) otherSimpleFields.get(field); + } + + /** + * Set generic VCard field. + * + * @param value value of field + * @param field field to set. See {@link #getField(String)} + * @see #getField(String) + */ + public void setField(String field, String value) { + otherSimpleFields.put(field, value); + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getMiddleName() { + return middleName; + } + + public void setMiddleName(String middleName) { + this.middleName = middleName; + } + + public String getNickName() { + return (String) otherSimpleFields.get("NICKNAME"); + } + + public void setNickName(String nickName) { + otherSimpleFields.put("NICKNAME", nickName); + } + + public String getEmailHome() { + return emailHome; + } + + public void setEmailHome(String email) { + this.emailHome = email; + } + + public String getEmailWork() { + return emailWork; + } + + public void setEmailWork(String emailWork) { + this.emailWork = emailWork; + } + + public String getJabberId() { + return (String) otherSimpleFields.get("JABBERID"); + } + + public void setJabberId(String jabberId) { + otherSimpleFields.put("JABBERID", jabberId); + } + + public String getOrganization() { + return organization; + } + + public void setOrganization(String organization) { + this.organization = organization; + } + + public String getOrganizationUnit() { + return organizationUnit; + } + + public void setOrganizationUnit(String organizationUnit) { + this.organizationUnit = organizationUnit; + } + + /** + * Get home address field + * + * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET, + * LOCALITY, REGION, PCODE, CTRY + */ + public String getAddressFieldHome(String addrField) { + return (String) homeAddr.get(addrField); + } + + /** + * Set home address field + * + * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET, + * LOCALITY, REGION, PCODE, CTRY + */ + public void setAddressFieldHome(String addrField, String value) { + homeAddr.put(addrField, value); + } + + /** + * Get work address field + * + * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET, + * LOCALITY, REGION, PCODE, CTRY + */ + public String getAddressFieldWork(String addrField) { + return (String) workAddr.get(addrField); + } + + /** + * Set work address field + * + * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET, + * LOCALITY, REGION, PCODE, CTRY + */ + public void setAddressFieldWork(String addrField, String value) { + workAddr.put(addrField, value); + } + + + /** + * Set home phone number + * + * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF + * @param phoneNum phone number + */ + public void setPhoneHome(String phoneType, String phoneNum) { + homePhones.put(phoneType, phoneNum); + } + + /** + * Get home phone number + * + * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF + */ + public String getPhoneHome(String phoneType) { + return (String) homePhones.get(phoneType); + } + + /** + * Set work phone number + * + * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF + * @param phoneNum phone number + */ + public void setPhoneWork(String phoneType, String phoneNum) { + workPhones.put(phoneType, phoneNum); + } + + /** + * Get work phone number + * + * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF + */ + public String getPhoneWork(String phoneType) { + return (String) workPhones.get(phoneType); + } + + /** + * Set the avatar for the VCard by specifying the url to the image. + * + * @param avatarURL the url to the image(png,jpeg,gif,bmp) + */ + public void setAvatar(URL avatarURL) { + byte[] bytes = new byte[0]; + try { + bytes = getBytes(avatarURL); + } + catch (IOException e) { + e.printStackTrace(); + } + + String encodedImage = StringUtils.encodeBase64(bytes); + avatar = encodedImage; + + setField("PHOTO", "<TYPE>image/jpeg</TYPE><BINVAL>" + encodedImage + "</BINVAL>"); + } + + /** + * Specify the bytes for the avatar to use. + * + * @param bytes the bytes of the avatar. + */ + public void setAvatar(byte[] bytes) { + String encodedImage = StringUtils.encodeBase64(bytes); + avatar = encodedImage; + + setField("PHOTO", "<TYPE>image/jpeg</TYPE><BINVAL>" + encodedImage + "</BINVAL>"); + } + + /** + * Set the encoded avatar string. This is used by the provider. + * + * @param encodedAvatar the encoded avatar string. + */ + public void setEncodedImage(String encodedAvatar) { + //TODO Move VCard and VCardProvider into a vCard package. + this.avatar = encodedAvatar; + } + + /** + * Return the byte representation of the avatar(if one exists), otherwise returns null if + * no avatar could be found. + * <b>Example 1</b> + * <pre> + * // Load Avatar from VCard + * byte[] avatarBytes = vCard.getAvatar(); + * <p/> + * // To create an ImageIcon for Swing applications + * ImageIcon icon = new ImageIcon(avatar); + * <p/> + * // To create just an image object from the bytes + * ByteArrayInputStream bais = new ByteArrayInputStream(avatar); + * try { + * Image image = ImageIO.read(bais); + * } + * catch (IOException e) { + * e.printStackTrace(); + * } + * </pre> + * + * @return byte representation of avatar. + */ + public byte[] getAvatar() { + if (avatar == null) { + return null; + } + if (avatar != null) { + return StringUtils.decodeBase64(avatar); + } + return null; + } + + /** + * Common code for getting the bytes of a url. + * + * @param url the url to read. + */ + public static byte[] getBytes(URL url) throws IOException { + final String path = url.getPath(); + final File file = new File(path); + if (file.exists()) { + return getFileBytes(file); + } + + return null; + } + + private static byte[] getFileBytes(File file) throws IOException { + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); + int bytes = (int) file.length(); + byte[] buffer = new byte[bytes]; + int readBytes = bis.read(buffer); + bis.close(); + return buffer; + } + + /** + * Returns the SHA-1 Hash of the Avatar image. + * + * @return the SHA-1 Hash of the Avatar image. + */ + public String getAvatarHash() { + byte[] bytes = getAvatar(); + if (bytes == null) { + return null; + } + + MessageDigest digest = null; + try { + digest = MessageDigest.getInstance("SHA-1"); + } + catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + + digest.update(bytes); + return StringUtils.encodeHex(digest.digest()); + } + + /** + * Save this vCard for the user connected by 'connection'. Connection should be authenticated + * and not anonymous.<p> + * <p/> + * NOTE: the method is asynchronous and does not wait for the returned value. + * @param connection the XMPPConnection to use. + * @throws XMPPException thrown if there was an issue setting the VCard in the server. + */ + public void save(XMPPConnection connection) throws XMPPException { + checkAuthenticated(connection); + + setType(IQ.Type.SET); + setFrom(connection.getUser()); + PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(getPacketID())); + connection.sendPacket(this); + + Packet response = collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Cancel the collector. + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from server on status set."); + } + if (response.getError() != null) { + throw new XMPPException(response.getError()); + } + } + + /** + * Load VCard information for a connected user. Connection should be authenticated + * and not anonymous. + */ + public void load(XMPPConnection connection) throws XMPPException { + checkAuthenticated(connection); + + setFrom(connection.getUser()); + doLoad(connection, connection.getUser()); + } + + /** + * Load VCard information for a given user. Connection should be authenticated and not anonymous. + */ + public void load(XMPPConnection connection, String user) throws XMPPException { + checkAuthenticated(connection); + + setTo(user); + doLoad(connection, user); + } + + private void doLoad(XMPPConnection connection, String user) throws XMPPException { + setType(Type.GET); + PacketCollector collector = connection.createPacketCollector( + new PacketIDFilter(getPacketID())); + connection.sendPacket(this); + + VCard result = null; + try { + result = (VCard) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + if (result == null) { + throw new XMPPException(new XMPPError(408, "Timeout getting VCard information")); + } + if (result.getError() != null) { + throw new XMPPException(result.getError()); + } + } + catch (ClassCastException e) { + System.out.println("No VCard for " + user); + } + + copyFieldsFrom(result); + } + + public String getChildElementXML() { + StringBuffer sb = new StringBuffer(); + new VCardWriter(sb).write(); + return sb.toString(); + } + + private void copyFieldsFrom(VCard result) { + if (result == null) result = new VCard(); + + Field[] fields = VCard.class.getDeclaredFields(); + for (int i = 0; i < fields.length; i++) { + Field field = fields[i]; + if (field.getDeclaringClass() == VCard.class && + !Modifier.isFinal(field.getModifiers())) { + try { + field.setAccessible(true); + field.set(this, field.get(result)); + } + catch (IllegalAccessException e) { + throw new RuntimeException("This cannot happen:" + field, e); + } + } + } + } + + private void checkAuthenticated(XMPPConnection connection) { + if (connection == null) { + new IllegalArgumentException("No connection was provided"); + } + if (!connection.isAuthenticated()) { + new IllegalArgumentException("Connection is not authenticated"); + } + if (connection.isAnonymous()) { + new IllegalArgumentException("Connection cannot be anonymous"); + } + } + + private boolean hasContent() { + //noinspection OverlyComplexBooleanExpression + return hasNameField() + || hasOrganizationFields() + || emailHome != null + || emailWork != null + || otherSimpleFields.size() > 0 + || homeAddr.size() > 0 + || homePhones.size() > 0 + || workAddr.size() > 0 + || workPhones.size() > 0 + ; + } + + private boolean hasNameField() { + return firstName != null || lastName != null || middleName != null; + } + + private boolean hasOrganizationFields() { + return organization != null || organizationUnit != null; + } + + // Used in tests: + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final VCard vCard = (VCard) o; + + if (emailHome != null ? !emailHome.equals(vCard.emailHome) : vCard.emailHome != null) { + return false; + } + if (emailWork != null ? !emailWork.equals(vCard.emailWork) : vCard.emailWork != null) { + return false; + } + if (firstName != null ? !firstName.equals(vCard.firstName) : vCard.firstName != null) { + return false; + } + if (!homeAddr.equals(vCard.homeAddr)) { + return false; + } + if (!homePhones.equals(vCard.homePhones)) { + return false; + } + if (lastName != null ? !lastName.equals(vCard.lastName) : vCard.lastName != null) { + return false; + } + if (middleName != null ? !middleName.equals(vCard.middleName) : vCard.middleName != null) { + return false; + } + if (organization != null ? + !organization.equals(vCard.organization) : vCard.organization != null) { + return false; + } + if (organizationUnit != null ? + !organizationUnit.equals(vCard.organizationUnit) : vCard.organizationUnit != null) { + return false; + } + if (!otherSimpleFields.equals(vCard.otherSimpleFields)) { + return false; + } + if (!workAddr.equals(vCard.workAddr)) { + return false; + } + if (!workPhones.equals(vCard.workPhones)) { + return false; + } + + return true; + } + + public int hashCode() { + int result; + result = homePhones.hashCode(); + result = 29 * result + workPhones.hashCode(); + result = 29 * result + homeAddr.hashCode(); + result = 29 * result + workAddr.hashCode(); + result = 29 * result + (firstName != null ? firstName.hashCode() : 0); + result = 29 * result + (lastName != null ? lastName.hashCode() : 0); + result = 29 * result + (middleName != null ? middleName.hashCode() : 0); + result = 29 * result + (emailHome != null ? emailHome.hashCode() : 0); + result = 29 * result + (emailWork != null ? emailWork.hashCode() : 0); + result = 29 * result + (organization != null ? organization.hashCode() : 0); + result = 29 * result + (organizationUnit != null ? organizationUnit.hashCode() : 0); + result = 29 * result + otherSimpleFields.hashCode(); + return result; + } + + public String toString() { + return getChildElementXML(); + } + + //============================================================== + + private class VCardWriter { + + private final StringBuffer sb; + + VCardWriter(StringBuffer sb) { + this.sb = sb; + } + + public void write() { + appendTag("vCard", "xmlns", "vcard-temp", hasContent(), new ContentBuilder() { + public void addTagContent() { + buildActualContent(); + } + }); + } + + private void buildActualContent() { + if (hasNameField()) { + appendFN(); + appendN(); + } + + appendOrganization(); + appendGenericFields(); + + appendEmail(emailWork, "WORK"); + appendEmail(emailHome, "HOME"); + + appendPhones(workPhones, "WORK"); + appendPhones(homePhones, "HOME"); + + appendAddress(workAddr, "WORK"); + appendAddress(homeAddr, "HOME"); + } + + private void appendEmail(final String email, final String type) { + if (email != null) { + appendTag("EMAIL", true, new ContentBuilder() { + public void addTagContent() { + appendEmptyTag(type); + appendEmptyTag("INTERNET"); + appendEmptyTag("PREF"); + appendTag("USERID", email); + } + }); + } + } + + private void appendPhones(Map phones, final String code) { + Iterator it = phones.entrySet().iterator(); + while (it.hasNext()) { + final Map.Entry entry = (Map.Entry) it.next(); + appendTag("TEL", true, new ContentBuilder() { + public void addTagContent() { + appendEmptyTag(entry.getKey()); + appendEmptyTag(code); + appendTag("NUMBER", (String) entry.getValue()); + } + }); + } + } + + private void appendAddress(final Map addr, final String code) { + if (addr.size() > 0) { + appendTag("ADR", true, new ContentBuilder() { + public void addTagContent() { + appendEmptyTag(code); + + Iterator it = addr.entrySet().iterator(); + while (it.hasNext()) { + final Map.Entry entry = (Map.Entry) it.next(); + appendTag((String) entry.getKey(), (String) entry.getValue()); + } + } + }); + } + } + + private void appendEmptyTag(Object tag) { + sb.append('<').append(tag).append("/>"); + } + + private void appendGenericFields() { + Iterator it = otherSimpleFields.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = (Map.Entry) it.next(); + appendTag(entry.getKey().toString(), (String) entry.getValue()); + } + } + + private void appendOrganization() { + if (hasOrganizationFields()) { + appendTag("ORG", true, new ContentBuilder() { + public void addTagContent() { + appendTag("ORGNAME", organization); + appendTag("ORGUNIT", organizationUnit); + } + }); + } + } + + private void appendField(String tag) { + String value = (String) otherSimpleFields.get(tag); + appendTag(tag, value); + } + + private void appendFN() { + final ContentBuilder contentBuilder = new ContentBuilder() { + public void addTagContent() { + if (firstName != null) { + sb.append(firstName + ' '); + } + if (middleName != null) { + sb.append(middleName + ' '); + } + if (lastName != null) { + sb.append(lastName); + } + } + }; + appendTag("FN", true, contentBuilder); + } + + private void appendN() { + appendTag("N", true, new ContentBuilder() { + public void addTagContent() { + appendTag("FAMILY", lastName); + appendTag("GIVEN", firstName); + appendTag("MIDDLE", middleName); + } + }); + } + + private void appendTag(String tag, String attr, String attrValue, boolean hasContent, + ContentBuilder builder) { + sb.append('<').append(tag); + if (attr != null) { + sb.append(' ').append(attr).append('=').append('\'').append(attrValue).append('\''); + } + + if (hasContent) { + sb.append('>'); + builder.addTagContent(); + sb.append("</").append(tag).append(">\n"); + } + else { + sb.append("/>\n"); + } + } + + private void appendTag(String tag, boolean hasContent, ContentBuilder builder) { + appendTag(tag, null, null, hasContent, builder); + } + + private void appendTag(String tag, final String tagText) { + if (tagText == null) return; + final ContentBuilder contentBuilder = new ContentBuilder() { + public void addTagContent() { + sb.append(tagText.trim()); + } + }; + appendTag(tag, true, contentBuilder); + } + + } + + //============================================================== + + private interface ContentBuilder { + + void addTagContent(); + } + + //============================================================== +} + diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Version.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Version.java new file mode 100644 index 000000000..206208f3f --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/Version.java @@ -0,0 +1,132 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; + +/** + * A Version IQ packet, which is used by XMPP clients to discover version information + * about the software running at another entity's JID.<p> + * + * An example to discover the version of the server: + * <pre> + * // Request the version from the server. + * Version versionRequest = new Version(); + * timeRequest.setType(IQ.Type.GET); + * timeRequest.setTo("example.com"); + * + * // Create a packet collector to listen for a response. + * PacketCollector collector = con.createPacketCollector( + * new PacketIDFilter(versionRequest.getPacketID())); + * + * con.sendPacket(versionRequest); + * + * // Wait up to 5 seconds for a result. + * IQ result = (IQ)collector.nextResult(5000); + * if (result != null && result.getType() == IQ.Type.RESULT) { + * Version versionResult = (Version)result; + * // Do something with result... + * }</pre><p> + * + * @author Gaston Dombiak + */ +public class Version extends IQ { + + private String name; + private String version; + private String os; + + /** + * Returns the natural-language name of the software. This property will always be + * present in a result. + * + * @return the natural-language name of the software. + */ + public String getName() { + return name; + } + + /** + * Sets the natural-language name of the software. This message should only be + * invoked when parsing the XML and setting the property to a Version instance. + * + * @param name the natural-language name of the software. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the specific version of the software. This property will always be + * present in a result. + * + * @return the specific version of the software. + */ + public String getVersion() { + return version; + } + + /** + * Sets the specific version of the software. This message should only be + * invoked when parsing the XML and setting the property to a Version instance. + * + * @param version the specific version of the software. + */ + public void setVersion(String version) { + this.version = version; + } + + /** + * Returns the operating system of the queried entity. This property will always be + * present in a result. + * + * @return the operating system of the queried entity. + */ + public String getOs() { + return os; + } + + /** + * Sets the operating system of the queried entity. This message should only be + * invoked when parsing the XML and setting the property to a Version instance. + * + * @param os operating system of the queried entity. + */ + public void setOs(String os) { + this.os = os; + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"jabber:iq:version\">"); + if (name != null) { + buf.append("<name>").append(name).append("</name>"); + } + if (version != null) { + buf.append("<version>").append(version).append("</version>"); + } + if (os != null) { + buf.append("<os>").append(os).append("</os>"); + } + buf.append("</query>"); + return buf.toString(); + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/XHTMLExtension.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/XHTMLExtension.java new file mode 100644 index 000000000..6bf266569 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/XHTMLExtension.java @@ -0,0 +1,123 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.packet; + +import java.util.*; + +import org.jivesoftware.smack.packet.PacketExtension; + +/** + * An XHTML sub-packet, which is used by XMPP clients to exchange formatted text. The XHTML + * extension is only a subset of XHTML 1.0.<p> + * + * The following link summarizes the requirements of XHTML IM: + * <a href="http://www.jabber.org/jeps/jep-0071.html#sect-id2598018">Valid tags</a>.<p> + * + * Warning: this is an non-standard protocol documented by + * <a href="http://www.jabber.org/jeps/jep-0071.html">JEP-71</a>. Because this is a + * non-standard protocol, it is subject to change. + * + * @author Gaston Dombiak + */ +public class XHTMLExtension implements PacketExtension { + + private List bodies = new ArrayList(); + + /** + * Returns the XML element name of the extension sub-packet root element. + * Always returns "html" + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return "html"; + } + + /** + * Returns the XML namespace of the extension sub-packet root element. + * According the specification the namespace is always "http://jabber.org/protocol/xhtml-im" + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return "http://jabber.org/protocol/xhtml-im"; + } + + /** + * Returns the XML representation of a XHTML extension according the specification. + * + * Usually the XML representation will be inside of a Message XML representation like + * in the following example: + * <pre> + * <message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"> + * <subject>Any subject you want</subject> + * <body>This message contains something interesting.</body> + * <html xmlns="http://jabber.org/protocol/xhtml-im"> + * <body><p style='font-size:large'>This message contains something <em>interesting</em>.</p></body> + * </html> + * </message> + * </pre> + * + */ + public String toXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\">"); + // Loop through all the bodies and append them to the string buffer + for (Iterator i = getBodies(); i.hasNext();) { + buf.append((String) i.next()); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + /** + * Returns an Iterator for the bodies in the packet. + * + * @return an Iterator for the bodies in the packet. + */ + public Iterator getBodies() { + synchronized (bodies) { + return Collections.unmodifiableList(new ArrayList(bodies)).iterator(); + } + } + + /** + * Adds a body to the packet. + * + * @param body the body to add. + */ + public void addBody(String body) { + synchronized (bodies) { + bodies.add(body); + } + } + + /** + * Returns a count of the bodies in the XHTML packet. + * + * @return the number of bodies in the XHTML packet. + */ + public int getBodiesCount() { + return bodies.size(); + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/package.html new file mode 100644 index 000000000..490d1d72d --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/packet/package.html @@ -0,0 +1 @@ +<body>XML packets that are part of the XMPP extension protocols.</body>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/BytestreamsProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/BytestreamsProvider.java new file mode 100644 index 000000000..0c42453fb --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/BytestreamsProvider.java @@ -0,0 +1,104 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.packet.Bytestream; +import org.xmlpull.v1.XmlPullParser; + +/** + * Parses a bytestream packet. + * + * @author Alexander Wenckus + */ +public class BytestreamsProvider implements IQProvider { + + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smack.provider.IQProvider#parseIQ(org.xmlpull.v1.XmlPullParser) + */ + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smack.provider.IQProvider#parseIQ(org.xmlpull.v1.XmlPullParser) + */ + /* + * (non-Javadoc) + * + * @see org.jivesoftware.smack.provider.IQProvider#parseIQ(org.xmlpull.v1.XmlPullParser) + */ + public IQ parseIQ(XmlPullParser parser) throws Exception { + // StringBuffer buf = new StringBuffer(); + boolean done = false; + + Bytestream toReturn = new Bytestream(); + + String id = parser.getAttributeValue("", "sid"); + String mode = parser.getAttributeValue("", "mode"); + + // streamhost + String JID = null; + String host = null; + String port = null; + + int eventType; + String elementName; + // String namespace; + while (!done) { + eventType = parser.next(); + elementName = parser.getName(); + // namespace = parser.getNamespace(); + if (eventType == XmlPullParser.START_TAG) { + if (elementName.equals(Bytestream.StreamHost.ELEMENTNAME)) { + JID = parser.getAttributeValue("", "jid"); + host = parser.getAttributeValue("", "host"); + port = parser.getAttributeValue("", "port"); + } else if (elementName + .equals(Bytestream.StreamHostUsed.ELEMENTNAME)) { + toReturn.setUsedHost(parser.getAttributeValue("", "jid")); + } else if (elementName.equals(Bytestream.Activate.ELEMENTNAME)) { + toReturn.setToActivate(parser.getAttributeValue("", "jid")); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (elementName.equals("streamhost")) { + if (port == null) { + toReturn.addStreamHost(JID, host); + } else { + toReturn.addStreamHost(JID, host, Integer + .parseInt(port)); + } + JID = null; + host = null; + port = null; + } else if (elementName.equals("query")) { + done = true; + } + } + } + + toReturn.setMode((mode == "udp" ? Bytestream.Mode.UDP + : Bytestream.Mode.TCP)); + toReturn.setSessionID(id); + return toReturn; + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DataFormProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DataFormProvider.java new file mode 100644 index 000000000..325a9ce2d --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DataFormProvider.java @@ -0,0 +1,160 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.packet.DataForm; +import org.xmlpull.v1.XmlPullParser; + +/** + * The DataFormProvider parses DataForm packets. + * + * @author Gaston Dombiak + */ +public class DataFormProvider implements PacketExtensionProvider { + + /** + * Creates a new DataFormProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor + */ + public DataFormProvider() { + } + + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + boolean done = false; + StringBuffer buffer = null; + DataForm dataForm = new DataForm(parser.getAttributeValue("", "type")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("instructions")) { + dataForm.addInstruction(parser.nextText()); + } + else if (parser.getName().equals("title")) { + dataForm.setTitle(parser.nextText()); + } + else if (parser.getName().equals("field")) { + dataForm.addField(parseField(parser)); + } + else if (parser.getName().equals("item")) { + dataForm.addItem(parseItem(parser)); + } + else if (parser.getName().equals("reported")) { + dataForm.setReportedData(parseReported(parser)); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(dataForm.getElementName())) { + done = true; + } + } + } + return dataForm; + } + + private FormField parseField(XmlPullParser parser) throws Exception { + boolean done = false; + FormField formField = new FormField(parser.getAttributeValue("", "var")); + formField.setLabel(parser.getAttributeValue("", "label")); + formField.setType(parser.getAttributeValue("", "type")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("desc")) { + formField.setDescription(parser.nextText()); + } + else if (parser.getName().equals("value")) { + formField.addValue(parser.nextText()); + } + else if (parser.getName().equals("required")) { + formField.setRequired(true); + } + else if (parser.getName().equals("option")) { + formField.addOption(parseOption(parser)); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("field")) { + done = true; + } + } + } + return formField; + } + + private DataForm.Item parseItem(XmlPullParser parser) throws Exception { + boolean done = false; + List fields = new ArrayList(); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("field")) { + fields.add(parseField(parser)); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + done = true; + } + } + } + return new DataForm.Item(fields); + } + + private DataForm.ReportedData parseReported(XmlPullParser parser) throws Exception { + boolean done = false; + List fields = new ArrayList(); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("field")) { + fields.add(parseField(parser)); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("reported")) { + done = true; + } + } + } + return new DataForm.ReportedData(fields); + } + + private FormField.Option parseOption(XmlPullParser parser) throws Exception { + boolean done = false; + FormField.Option option = null; + String label = parser.getAttributeValue("", "label"); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("value")) { + option = new FormField.Option(label, parser.nextText()); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("option")) { + done = true; + } + } + } + return option; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DelayInformationProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DelayInformationProvider.java new file mode 100644 index 000000000..28bab2367 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DelayInformationProvider.java @@ -0,0 +1,75 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.packet.DelayInformation; +import org.xmlpull.v1.XmlPullParser; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +/** + * The DelayInformationProvider parses DelayInformation packets. + * + * @author Gaston Dombiak + */ +public class DelayInformationProvider implements PacketExtensionProvider { + + /** + * Creates a new DeliveryInformationProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument + * constructor + */ + public DelayInformationProvider() { + } + + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + Date stamp = null; + try { + synchronized (DelayInformation.UTC_FORMAT) { + stamp = DelayInformation.UTC_FORMAT.parse(parser.getAttributeValue("", "stamp")); + } + } catch (ParseException e) { + // Try again but assuming that the date follows JEP-82 format + // (Jabber Date and Time Profiles) + try { + synchronized (DelayInformation.NEW_UTC_FORMAT) { + stamp = DelayInformation.NEW_UTC_FORMAT + .parse(parser.getAttributeValue("", "stamp")); + } + } catch (ParseException e1) { + // Last attempt. Try parsing the date assuming that it does not include milliseconds + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + stamp = formatter.parse(parser.getAttributeValue("", "stamp")); + } + } + DelayInformation delayInformation = new DelayInformation(stamp); + delayInformation.setFrom(parser.getAttributeValue("", "from")); + delayInformation.setReason(parser.nextText()); + return delayInformation; + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java new file mode 100644 index 000000000..cf13d63f5 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java @@ -0,0 +1,83 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.xmlpull.v1.XmlPullParser; + +/** +* The DiscoverInfoProvider parses Service Discovery information packets. +* +* @author Gaston Dombiak +*/ +public class DiscoverInfoProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + DiscoverInfo discoverInfo = new DiscoverInfo(); + boolean done = false; + DiscoverInfo.Feature feature = null; + DiscoverInfo.Identity identity = null; + String category = ""; + String name = ""; + String type = ""; + String variable = ""; + discoverInfo.setNode(parser.getAttributeValue("", "node")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("identity")) { + // Initialize the variables from the parsed XML + category = parser.getAttributeValue("", "category"); + name = parser.getAttributeValue("", "name"); + type = parser.getAttributeValue("", "type"); + } + else if (parser.getName().equals("feature")) { + // Initialize the variables from the parsed XML + variable = parser.getAttributeValue("", "var"); + } + // Otherwise, it must be a packet extension. + else { + discoverInfo.addExtension(PacketParserUtils.parsePacketExtension(parser + .getName(), parser.getNamespace(), parser)); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("identity")) { + // Create a new identity and add it to the discovered info. + identity = new DiscoverInfo.Identity(category, name); + identity.setType(type); + discoverInfo.addIdentity(identity); + } + if (parser.getName().equals("feature")) { + // Create a new feature and add it to the discovered info. + discoverInfo.addFeature(variable); + } + if (parser.getName().equals("query")) { + done = true; + } + } + } + + return discoverInfo; + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverItemsProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverItemsProvider.java new file mode 100644 index 000000000..34aae227b --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/DiscoverItemsProvider.java @@ -0,0 +1,71 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.packet.*; +import org.xmlpull.v1.XmlPullParser; + +/** +* The DiscoverInfoProvider parses Service Discovery items packets. +* +* @author Gaston Dombiak +*/ +public class DiscoverItemsProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + DiscoverItems discoverItems = new DiscoverItems(); + boolean done = false; + DiscoverItems.Item item = null; + String jid = ""; + String name = ""; + String action = ""; + String node = ""; + discoverItems.setNode(parser.getAttributeValue("", "node")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + // Initialize the variables from the parsed XML + jid = parser.getAttributeValue("", "jid"); + name = parser.getAttributeValue("", "name"); + node = parser.getAttributeValue("", "node"); + action = parser.getAttributeValue("", "action"); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + // Create a new Item and add it to DiscoverItems. + item = new DiscoverItems.Item(jid); + item.setName(name); + item.setNode(node); + item.setAction(action); + discoverItems.addItem(item); + } + if (parser.getName().equals("query")) { + done = true; + } + } + } + + return discoverItems; + } +}
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/IBBProviders.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/IBBProviders.java new file mode 100644 index 000000000..68af0e2c6 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/IBBProviders.java @@ -0,0 +1,85 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.packet.IBBExtensions; +import org.xmlpull.v1.XmlPullParser; + +/** + * + * Parses an IBB packet. + * + * @author Alexander Wenckus + */ +public class IBBProviders { + + /** + * Parses an open IBB packet. + * + * @author Alexander Wenckus + * + */ + public static class Open implements IQProvider { + public IQ parseIQ(XmlPullParser parser) throws Exception { + final String sid = parser.getAttributeValue("", "sid"); + final int blockSize = Integer.parseInt(parser.getAttributeValue("", + "block-size")); + + return new IBBExtensions.Open(sid, blockSize); + } + } + + /** + * Parses a data IBB packet. + * + * @author Alexander Wenckus + * + */ + public static class Data implements PacketExtensionProvider { + public PacketExtension parseExtension(XmlPullParser parser) + throws Exception { + final String sid = parser.getAttributeValue("", "sid"); + final long seq = Long + .parseLong(parser.getAttributeValue("", "seq")); + final String data = parser.nextText(); + + return new IBBExtensions.Data(sid, seq, data); + } + } + + /** + * Parses a close IBB packet. + * + * @author Alexander Wenckus + * + */ + public static class Close implements IQProvider { + public IQ parseIQ(XmlPullParser parser) throws Exception { + final String sid = parser.getAttributeValue("", "sid"); + + return new IBBExtensions.Close(sid); + } + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCAdminProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCAdminProvider.java new file mode 100644 index 000000000..0f5f04f07 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCAdminProvider.java @@ -0,0 +1,81 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.packet.MUCAdmin; +import org.xmlpull.v1.XmlPullParser; + +/** + * The MUCAdminProvider parses MUCAdmin packets. (@see MUCAdmin) + * + * @author Gaston Dombiak + */ +public class MUCAdminProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + MUCAdmin mucAdmin = new MUCAdmin(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + mucAdmin.addItem(parseItem(parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + + return mucAdmin; + } + + private MUCAdmin.Item parseItem(XmlPullParser parser) throws Exception { + boolean done = false; + MUCAdmin.Item item = + new MUCAdmin.Item( + parser.getAttributeValue("", "affiliation"), + parser.getAttributeValue("", "role")); + item.setNick(parser.getAttributeValue("", "nick")); + item.setJid(parser.getAttributeValue("", "jid")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("actor")) { + item.setActor(parser.getAttributeValue("", "jid")); + } + if (parser.getName().equals("reason")) { + item.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + done = true; + } + } + } + return item; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCOwnerProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCOwnerProvider.java new file mode 100644 index 000000000..aaa6afa88 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCOwnerProvider.java @@ -0,0 +1,108 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smack.provider.*; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.packet.MUCOwner; +import org.xmlpull.v1.XmlPullParser; + +/** + * The MUCOwnerProvider parses MUCOwner packets. (@see MUCOwner) + * + * @author Gaston Dombiak + */ +public class MUCOwnerProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + MUCOwner mucOwner = new MUCOwner(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + mucOwner.addItem(parseItem(parser)); + } + else if (parser.getName().equals("destroy")) { + mucOwner.setDestroy(parseDestroy(parser)); + } + // Otherwise, it must be a packet extension. + else { + mucOwner.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(), + parser.getNamespace(), parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + + return mucOwner; + } + + private MUCOwner.Item parseItem(XmlPullParser parser) throws Exception { + boolean done = false; + MUCOwner.Item item = new MUCOwner.Item(parser.getAttributeValue("", "affiliation")); + item.setNick(parser.getAttributeValue("", "nick")); + item.setRole(parser.getAttributeValue("", "role")); + item.setJid(parser.getAttributeValue("", "jid")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("actor")) { + item.setActor(parser.getAttributeValue("", "jid")); + } + if (parser.getName().equals("reason")) { + item.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + done = true; + } + } + } + return item; + } + + private MUCOwner.Destroy parseDestroy(XmlPullParser parser) throws Exception { + boolean done = false; + MUCOwner.Destroy destroy = new MUCOwner.Destroy(); + destroy.setJid(parser.getAttributeValue("", "jid")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("reason")) { + destroy.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("destroy")) { + done = true; + } + } + } + return destroy; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCUserProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCUserProvider.java new file mode 100644 index 000000000..25b6e8d6b --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MUCUserProvider.java @@ -0,0 +1,174 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smack.provider.*; +import org.jivesoftware.smackx.packet.*; +import org.xmlpull.v1.XmlPullParser; + +/** + * The MUCUserProvider parses packets with extended presence information about + * roles and affiliations. + * + * @author Gaston Dombiak + */ +public class MUCUserProvider implements PacketExtensionProvider { + + /** + * Creates a new MUCUserProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument + * constructor + */ + public MUCUserProvider() { + } + + /** + * Parses a MUCUser packet (extension sub-packet). + * + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + MUCUser mucUser = new MUCUser(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("invite")) { + mucUser.setInvite(parseInvite(parser)); + } + if (parser.getName().equals("item")) { + mucUser.setItem(parseItem(parser)); + } + if (parser.getName().equals("password")) { + mucUser.setPassword(parser.nextText()); + } + if (parser.getName().equals("status")) { + mucUser.setStatus(new MUCUser.Status(parser.getAttributeValue("", "code"))); + } + if (parser.getName().equals("decline")) { + mucUser.setDecline(parseDecline(parser)); + } + if (parser.getName().equals("destroy")) { + mucUser.setDestroy(parseDestroy(parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("x")) { + done = true; + } + } + } + + return mucUser; + } + + private MUCUser.Item parseItem(XmlPullParser parser) throws Exception { + boolean done = false; + MUCUser.Item item = + new MUCUser.Item( + parser.getAttributeValue("", "affiliation"), + parser.getAttributeValue("", "role")); + item.setNick(parser.getAttributeValue("", "nick")); + item.setJid(parser.getAttributeValue("", "jid")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("actor")) { + item.setActor(parser.getAttributeValue("", "jid")); + } + if (parser.getName().equals("reason")) { + item.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + done = true; + } + } + } + return item; + } + + private MUCUser.Invite parseInvite(XmlPullParser parser) throws Exception { + boolean done = false; + MUCUser.Invite invite = new MUCUser.Invite(); + invite.setFrom(parser.getAttributeValue("", "from")); + invite.setTo(parser.getAttributeValue("", "to")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("reason")) { + invite.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("invite")) { + done = true; + } + } + } + return invite; + } + + private MUCUser.Decline parseDecline(XmlPullParser parser) throws Exception { + boolean done = false; + MUCUser.Decline decline = new MUCUser.Decline(); + decline.setFrom(parser.getAttributeValue("", "from")); + decline.setTo(parser.getAttributeValue("", "to")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("reason")) { + decline.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("decline")) { + done = true; + } + } + } + return decline; + } + + private MUCUser.Destroy parseDestroy(XmlPullParser parser) throws Exception { + boolean done = false; + MUCUser.Destroy destroy = new MUCUser.Destroy(); + destroy.setJid(parser.getAttributeValue("", "jid")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("reason")) { + destroy.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("destroy")) { + done = true; + } + } + } + return destroy; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MessageEventProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MessageEventProvider.java new file mode 100644 index 000000000..b93a0b9a4 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MessageEventProvider.java @@ -0,0 +1,77 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.packet.MessageEvent; +import org.xmlpull.v1.XmlPullParser; + +/** + * + * The MessageEventProvider parses Message Event packets. +* + * @author Gaston Dombiak + */ +public class MessageEventProvider implements PacketExtensionProvider { + + /** + * Creates a new MessageEventProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor + */ + public MessageEventProvider() { + } + + /** + * Parses a MessageEvent packet (extension sub-packet). + * + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public PacketExtension parseExtension(XmlPullParser parser) + throws Exception { + MessageEvent messageEvent = new MessageEvent(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("id")) + messageEvent.setPacketID(parser.nextText()); + if (parser.getName().equals(MessageEvent.COMPOSING)) + messageEvent.setComposing(true); + if (parser.getName().equals(MessageEvent.DELIVERED)) + messageEvent.setDelivered(true); + if (parser.getName().equals(MessageEvent.DISPLAYED)) + messageEvent.setDisplayed(true); + if (parser.getName().equals(MessageEvent.OFFLINE)) + messageEvent.setOffline(true); + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("x")) { + done = true; + } + } + } + + return messageEvent; + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MultipleAddressesProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MultipleAddressesProvider.java new file mode 100644 index 000000000..4c3e35665 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/MultipleAddressesProvider.java @@ -0,0 +1,67 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.packet.MultipleAddresses; +import org.xmlpull.v1.XmlPullParser; + +/** + * The MultipleAddressesProvider parses {@link MultipleAddresses} packets. + * + * @author Gaston Dombiak + */ +public class MultipleAddressesProvider implements PacketExtensionProvider { + + /** + * Creates a new MultipleAddressesProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument + * constructor. + */ + public MultipleAddressesProvider() { + } + + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + boolean done = false; + MultipleAddresses multipleAddresses = new MultipleAddresses(); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("address")) { + String type = parser.getAttributeValue("", "type"); + String jid = parser.getAttributeValue("", "jid"); + String node = parser.getAttributeValue("", "node"); + String desc = parser.getAttributeValue("", "desc"); + boolean delivered = "true".equals(parser.getAttributeValue("", "delivered")); + String uri = parser.getAttributeValue("", "uri"); + // Add the parsed address + multipleAddresses.addAddress(type, jid, node, desc, delivered, uri); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(multipleAddresses.getElementName())) { + done = true; + } + } + } + return multipleAddresses; + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/PrivateDataProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/PrivateDataProvider.java new file mode 100644 index 000000000..7961da1bc --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/PrivateDataProvider.java @@ -0,0 +1,46 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.xmlpull.v1.XmlPullParser; +import org.jivesoftware.smackx.packet.PrivateData; + +/** + * An interface for parsing custom private data. Each PrivateDataProvider must + * be registered with the PrivateDataManager class for it to be used. Every implementation + * of this interface <b>must</b> have a public, no-argument constructor. + * + * @author Matt Tucker + */ +public interface PrivateDataProvider { + + /** + * Parse the private data sub-document and create a PrivateData instance. At the + * beginning of the method call, the xml parser will be positioned at the opening + * tag of the private data child element. At the end of the method call, the parser + * <b>must</b> be positioned on the closing tag of the child element. + * + * @param parser an XML parser. + * @return a new PrivateData instance. + * @throws Exception if an error occurs parsing the XML. + */ + public PrivateData parsePrivateData(XmlPullParser parser) throws Exception; +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/RosterExchangeProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/RosterExchangeProvider.java new file mode 100644 index 000000000..956e1328d --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/RosterExchangeProvider.java @@ -0,0 +1,90 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import java.util.ArrayList; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.*; +import org.jivesoftware.smackx.packet.*; +import org.xmlpull.v1.XmlPullParser; + +/** + * + * The RosterExchangeProvider parses RosterExchange packets. + * + * @author Gaston Dombiak + */ +public class RosterExchangeProvider implements PacketExtensionProvider { + + /** + * Creates a new RosterExchangeProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor + */ + public RosterExchangeProvider() { + } + + /** + * Parses a RosterExchange packet (extension sub-packet). + * + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + + RosterExchange rosterExchange = new RosterExchange(); + boolean done = false; + RemoteRosterEntry remoteRosterEntry = null; + String jid = ""; + String name = ""; + ArrayList groupsName = new ArrayList(); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + // Reset this variable since they are optional for each item + groupsName = new ArrayList(); + // Initialize the variables from the parsed XML + jid = parser.getAttributeValue("", "jid"); + name = parser.getAttributeValue("", "name"); + } + if (parser.getName().equals("group")) { + groupsName.add(parser.nextText()); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + // Create packet. + remoteRosterEntry = new RemoteRosterEntry(jid, name, (String[]) groupsName.toArray(new String[groupsName.size()])); + rosterExchange.addRosterEntry(remoteRosterEntry); + } + if (parser.getName().equals("x")) { + done = true; + } + } + } + + return rosterExchange; + + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/StreamInitiationProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/StreamInitiationProvider.java new file mode 100644 index 000000000..9a7106576 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/StreamInitiationProvider.java @@ -0,0 +1,112 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.packet.DataForm; +import org.jivesoftware.smackx.packet.DelayInformation; +import org.jivesoftware.smackx.packet.StreamInitiation; +import org.jivesoftware.smackx.packet.StreamInitiation.File; +import org.xmlpull.v1.XmlPullParser; + +/** + * The StreamInitiationProvider parses StreamInitiation packets. + * + * @author Alexander Wenckus + * + */ +public class StreamInitiationProvider implements IQProvider { + + public IQ parseIQ(final XmlPullParser parser) throws Exception { + boolean done = false; + + // si + String id = parser.getAttributeValue("", "id"); + String mimeType = parser.getAttributeValue("", "mime-type"); + + StreamInitiation initiation = new StreamInitiation(); + + // file + String name = null; + String size = null; + String hash = null; + String date = null; + String desc = null; + boolean isRanged = false; + + // feature + DataForm form = null; + DataFormProvider dataFormProvider = new DataFormProvider(); + + int eventType; + String elementName; + String namespace; + while (!done) { + eventType = parser.next(); + elementName = parser.getName(); + namespace = parser.getNamespace(); + if (eventType == XmlPullParser.START_TAG) { + if (elementName.equals("file")) { + name = parser.getAttributeValue("", "name"); + size = parser.getAttributeValue("", "size"); + hash = parser.getAttributeValue("", "hash"); + date = parser.getAttributeValue("", "date"); + } else if (elementName.equals("desc")) { + desc = parser.nextText(); + } else if (elementName.equals("range")) { + isRanged = true; + } else if (elementName.equals("x") + && namespace.equals("jabber:x:data")) { + form = (DataForm) dataFormProvider.parseExtension(parser); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (elementName.equals("si")) { + done = true; + } else if (elementName.equals("file")) { + long fileSize = 0; + if(size != null && size.trim().length() !=0){ + try { + fileSize = Long.parseLong(size); + } + catch (NumberFormatException e) { + e.printStackTrace(); + } + } + File file = new File(name, fileSize); + file.setHash(hash); + if (date != null) + file.setDate(DelayInformation.UTC_FORMAT.parse(date)); + file.setDesc(desc); + file.setRanged(isRanged); + initiation.setFile(file); + } + } + } + + initiation.setSesssionID(id); + initiation.setMimeType(mimeType); + + initiation.setFeatureNegotiationForm(form); + + return initiation; + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/VCardProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/VCardProvider.java new file mode 100644 index 000000000..af45448b3 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/VCardProvider.java @@ -0,0 +1,244 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.packet.VCard; +import org.w3c.dom.*; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.List; +import java.util.ArrayList; + +/** + * vCard provider. + * + * @author Gaston Dombiak + */ +public class VCardProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + StringBuffer sb = new StringBuffer(); + try { + int event = parser.getEventType(); + // get the content + while (true) { + switch (event) { + case XmlPullParser.TEXT: + sb.append(parser.getText()); + break; + case XmlPullParser.START_TAG: + sb.append('<').append(parser.getName()).append('>'); + break; + case XmlPullParser.END_TAG: + sb.append("</").append(parser.getName()).append('>'); + break; + default: + } + + if (event == XmlPullParser.END_TAG && "vCard".equals(parser.getName())) break; + + event = parser.next(); + } + } catch (XmlPullParserException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + String xmlText = sb.toString(); + return _createVCardFromXml(xmlText); + } + + public static VCard _createVCardFromXml(String xmlText) { + VCard vCard = new VCard(); + try { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document document = documentBuilder.parse(new ByteArrayInputStream(xmlText.getBytes())); + + new VCardReader(vCard, document).initializeFields(); + + } catch (Exception e) { + e.printStackTrace(System.err); + } + return vCard; + } + + private static class VCardReader { + private final VCard vCard; + private final Document document; + + VCardReader(VCard vCard, Document document) { + this.vCard = vCard; + this.document = document; + } + + public void initializeFields() { + vCard.setFirstName(getTagContents("GIVEN")); + vCard.setLastName(getTagContents("FAMILY")); + vCard.setMiddleName(getTagContents("MIDDLE")); + vCard.setEncodedImage(getTagContents("BINVAL")); + + setupEmails(); + + vCard.setOrganization(getTagContents("ORGNAME")); + vCard.setOrganizationUnit(getTagContents("ORGUNIT")); + + setupSimpleFields(); + + setupPhones(); + setupAddresses(); + } + + private void setupEmails() { + NodeList nodes = document.getElementsByTagName("USERID"); + if (nodes == null) return; + for (int i = 0; i < nodes.getLength(); i++) { + Element element = (Element) nodes.item(i); + if ("HOME".equals(element.getParentNode().getFirstChild().getNodeName())) { + vCard.setEmailHome(getTextContent(element)); + } else { + vCard.setEmailWork(getTextContent(element)); + } + } + } + + private void setupPhones() { + NodeList allPhones = document.getElementsByTagName("TEL"); + if (allPhones == null) return; + for (int i = 0; i < allPhones.getLength(); i++) { + NodeList nodes = allPhones.item(i).getChildNodes(); + String type = null; + String code = null; + String value = null; + for (int j = 0; j < nodes.getLength(); j++) { + Node node = nodes.item(j); + if (node.getNodeType() != Node.ELEMENT_NODE) continue; + String nodeName = node.getNodeName(); + if ("NUMBER".equals(nodeName)) { + value = getTextContent(node); + } + else if (isWorkHome(nodeName)) { + type = nodeName; + } + else { + code = nodeName; + } + } + if (code == null || value == null) continue; + if ("HOME".equals(type)) { + vCard.setPhoneHome(code, value); + } + else { // By default, setup work phone + vCard.setPhoneWork(code, value); + } + } + } + + private boolean isWorkHome(String nodeName) { + return "HOME".equals(nodeName) || "WORK".equals(nodeName); + } + + private void setupAddresses() { + NodeList allAddresses = document.getElementsByTagName("ADR"); + if (allAddresses == null) return; + for (int i = 0; i < allAddresses.getLength(); i++) { + Element addressNode = (Element) allAddresses.item(i); + + String type = null; + List code = new ArrayList(); + List value = new ArrayList(); + NodeList childNodes = addressNode.getChildNodes(); + for(int j = 0; j < childNodes.getLength(); j++) { + Node node = childNodes.item(j); + if (node.getNodeType() != Node.ELEMENT_NODE) continue; + String nodeName = node.getNodeName(); + if (isWorkHome(nodeName)) { + type = nodeName; + } + else { + code.add(nodeName); + value.add(getTextContent(node)); + } + } + for (int j = 0; j < value.size(); j++) { + if ("HOME".equals(type)) { + vCard.setAddressFieldHome((String) code.get(j), (String) value.get(j)); + } + else { // By default, setup work address + vCard.setAddressFieldWork((String) code.get(j), (String) value.get(j)); + } + } + } + } + + private String getTagContents(String tag) { + NodeList nodes = document.getElementsByTagName(tag); + if (nodes != null && nodes.getLength() == 1) { + return getTextContent(nodes.item(0)); + } + return null; + } + + private void setupSimpleFields() { + NodeList childNodes = document.getDocumentElement().getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node node = childNodes.item(i); + if (node instanceof Element) { + Element element = (Element) node; + + String field = element.getNodeName(); + if (element.getChildNodes().getLength() == 0) { + vCard.setField(field, ""); + } else if (element.getChildNodes().getLength() == 1 && + element.getChildNodes().item(0) instanceof Text) { + vCard.setField(field, getTextContent(element)); + } + } + } + } + + private String getTextContent(Node node) { + StringBuffer result = new StringBuffer(); + appendText(result, node); + return result.toString(); + } + + private void appendText(StringBuffer result, Node node) { + NodeList childNodes = node.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node nd = childNodes.item(i); + String nodeValue = nd.getNodeValue(); + if (nodeValue != null) { + result.append(nodeValue); + } + appendText(result, nd); + } + } + } +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/XHTMLExtensionProvider.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/XHTMLExtensionProvider.java new file mode 100644 index 000000000..7970c7907 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/XHTMLExtensionProvider.java @@ -0,0 +1,78 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2004 Jive Software. + * + * All rights reserved. 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 org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.packet.XHTMLExtension; +import org.xmlpull.v1.XmlPullParser; + +/** + * The XHTMLExtensionProvider parses XHTML packets. + * + * @author Gaston Dombiak + */ +public class XHTMLExtensionProvider implements PacketExtensionProvider { + + /** + * Creates a new XHTMLExtensionProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor + */ + public XHTMLExtensionProvider() { + } + + /** + * Parses a XHTMLExtension packet (extension sub-packet). + * + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public PacketExtension parseExtension(XmlPullParser parser) + throws Exception { + XHTMLExtension xhtmlExtension = new XHTMLExtension(); + boolean done = false; + StringBuffer buffer = new StringBuffer();; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("body")) + buffer = new StringBuffer(); + buffer.append(parser.getText()); + } else if (eventType == XmlPullParser.TEXT) { + if (buffer != null) buffer.append(parser.getText()); + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("body")) { + buffer.append(parser.getText()); + xhtmlExtension.addBody(buffer.toString()); + } + else if (parser.getName().equals(xhtmlExtension.getElementName())) { + done = true; + } + else + buffer.append(parser.getText()); + } + } + + return xhtmlExtension; + } + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/package.html b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/package.html new file mode 100644 index 000000000..962ba6372 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/provider/package.html @@ -0,0 +1 @@ +<body>Provides pluggable parsing logic for Smack extensions.</body>
\ No newline at end of file diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/SimpleUserSearch.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/SimpleUserSearch.java new file mode 100644 index 000000000..4ab810e89 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/SimpleUserSearch.java @@ -0,0 +1,145 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright (C) 1999-2005 Jive Software. All rights reserved. + * + * This software is the proprietary information of Jive Software. + * Use is subject to license terms. + */ +package org.jivesoftware.smackx.search; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.ReportedData; +import org.xmlpull.v1.XmlPullParser; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * SimpleUserSearch is used to support the non-dataform type of JEP 55. This provides + * the mechanism for allowing always type ReportedData to be returned by any search result, + * regardless of the form of the data returned from the server. + * + * @author Derek DeMoro + */ +class SimpleUserSearch extends IQ { + + private Form form; + private ReportedData data; + + public void setForm(Form form) { + this.form = form; + } + + public ReportedData getReportedData() { + return data; + } + + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"jabber:iq:search\">"); + buf.append(getItemsToSearch()); + buf.append("</query>"); + return buf.toString(); + } + + private String getItemsToSearch() { + StringBuffer buf = new StringBuffer(); + + if (form == null) { + form = Form.getFormFrom(this); + } + + if (form == null) { + return ""; + } + + Iterator fields = form.getFields(); + while (fields.hasNext()) { + FormField field = (FormField) fields.next(); + String name = field.getVariable(); + String value = getSingleValue(field); + if (value.trim().length() > 0) { + buf.append("<").append(name).append(">").append(value).append("</").append(name).append(">"); + } + } + + return buf.toString(); + } + + private static String getSingleValue(FormField formField) { + Iterator values = formField.getValues(); + while (values.hasNext()) { + return (String) values.next(); + } + return ""; + } + + protected void parseItems(XmlPullParser parser) throws Exception { + ReportedData data = new ReportedData(); + data.addColumn(new ReportedData.Column("JID", "jid", "text-single")); + + boolean done = false; + + List fields = new ArrayList(); + while (!done) { + if (parser.getAttributeCount() > 0) { + String jid = parser.getAttributeValue("", "jid"); + List valueList = new ArrayList(); + valueList.add(jid); + ReportedData.Field field = new ReportedData.Field("jid", valueList); + fields.add(field); + } + + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG && parser.getName().equals("item")) { + fields = new ArrayList(); + } + else if (eventType == XmlPullParser.END_TAG && parser.getName().equals("item")) { + ReportedData.Row row = new ReportedData.Row(fields); + data.addRow(row); + } + else if (eventType == XmlPullParser.START_TAG) { + String name = parser.getName(); + String value = parser.nextText(); + + List valueList = new ArrayList(); + valueList.add(value); + ReportedData.Field field = new ReportedData.Field(name, valueList); + fields.add(field); + + boolean exists = false; + Iterator cols = data.getColumns(); + while (cols.hasNext()) { + ReportedData.Column column = (ReportedData.Column) cols.next(); + if (column.getVariable().equals(name)) { + exists = true; + } + } + + // Column name should be the same + if (!exists) { + ReportedData.Column column = new ReportedData.Column(name, name, "text-single"); + data.addColumn(column); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + + } + + this.data = data; + } + + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearch.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearch.java new file mode 100644 index 000000000..400344530 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearch.java @@ -0,0 +1,249 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright (C) 1999-2005 Jive Software. All rights reserved. + * + * This software is the proprietary information of Jive Software. + * Use is subject to license terms. + */ +package org.jivesoftware.smackx.search; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.ReportedData; +import org.jivesoftware.smackx.packet.DataForm; +import org.xmlpull.v1.XmlPullParser; + +/** + * Implements the protocol currently used to search information repositories on the Jabber network. To date, the jabber:iq:search protocol + * has been used mainly to search for people who have registered with user directories (e.g., the "Jabber User Directory" hosted at users.jabber.org). + * However, the jabber:iq:search protocol is not limited to user directories, and could be used to search other Jabber information repositories + * (such as chatroom directories) or even to provide a Jabber interface to conventional search engines. + * <p/> + * The basic functionality is to query an information repository regarding the possible search fields, to send a search query, and to receive search results. + * + * @author Derek DeMoro + */ +public class UserSearch extends IQ { + + /** + * Creates a new instance of UserSearch. + */ + public UserSearch() { + } + + public String getChildElementXML() { + StringBuffer buf = new StringBuffer(); + buf.append("<query xmlns=\"jabber:iq:search\">"); + buf.append(getExtensionsXML()); + buf.append("</query>"); + return buf.toString(); + } + + /** + * Returns the form for all search fields supported by the search service. + * + * @param con the current XMPPConnection. + * @param searchService the search service to use. (ex. search.jivesoftware.com) + * @return the search form received by the server. + * @throws org.jivesoftware.smack.XMPPException + * thrown if a server error has occurred. + */ + public Form getSearchForm(XMPPConnection con, String searchService) throws XMPPException { + UserSearch search = new UserSearch(); + search.setType(IQ.Type.GET); + search.setTo(searchService); + + PacketCollector collector = con.createPacketCollector(new PacketIDFilter(search.getPacketID())); + con.sendPacket(search); + + IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Cancel the collector. + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from server on status set."); + } + if (response.getError() != null) { + throw new XMPPException(response.getError()); + } + return Form.getFormFrom(response); + } + + /** + * Sends the filled out answer form to be sent and queried by the search service. + * + * @param con the current XMPPConnection. + * @param searchForm the <code>Form</code> to send for querying. + * @param searchService the search service to use. (ex. search.jivesoftware.com) + * @return ReportedData the data found from the query. + * @throws org.jivesoftware.smack.XMPPException + * thrown if a server error has occurred. + */ + public ReportedData sendSearchForm(XMPPConnection con, Form searchForm, String searchService) throws XMPPException { + UserSearch search = new UserSearch(); + search.setType(IQ.Type.SET); + search.setTo(searchService); + search.addExtension(searchForm.getDataFormToSend()); + + PacketCollector collector = con.createPacketCollector(new PacketIDFilter(search.getPacketID())); + + con.sendPacket(search); + + IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Cancel the collector. + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from server on status set."); + } + if (response.getError() != null) { + return sendSimpleSearchForm(con, searchForm, searchService); + } + + + return ReportedData.getReportedDataFrom(response); + } + + /** + * Sends the filled out answer form to be sent and queried by the search service. + * + * @param con the current XMPPConnection. + * @param searchForm the <code>Form</code> to send for querying. + * @param searchService the search service to use. (ex. search.jivesoftware.com) + * @return ReportedData the data found from the query. + * @throws org.jivesoftware.smack.XMPPException + * thrown if a server error has occurred. + */ + public ReportedData sendSimpleSearchForm(XMPPConnection con, Form searchForm, String searchService) throws XMPPException { + SimpleUserSearch search = new SimpleUserSearch(); + search.setForm(searchForm); + search.setType(IQ.Type.SET); + search.setTo(searchService); + + PacketCollector collector = con.createPacketCollector(new PacketIDFilter(search.getPacketID())); + + con.sendPacket(search); + + IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Cancel the collector. + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from server on status set."); + } + if (response.getError() != null) { + throw new XMPPException(response.getError()); + } + + if (response instanceof SimpleUserSearch) { + return ((SimpleUserSearch) response).getReportedData(); + } + return null; + } + + /** + * Internal Search service Provider. + */ + public static class Provider implements IQProvider { + + /** + * Provider Constructor. + */ + public Provider() { + super(); + } + + public IQ parseIQ(XmlPullParser parser) throws Exception { + UserSearch search = null; + SimpleUserSearch simpleUserSearch = new SimpleUserSearch(); + + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG && parser.getName().equals("instructions")) { + buildDataForm(simpleUserSearch, parser.nextText(), parser); + return simpleUserSearch; + } + else if (eventType == XmlPullParser.START_TAG && parser.getName().equals("item")) { + simpleUserSearch.parseItems(parser); + return simpleUserSearch; + } + else if (eventType == XmlPullParser.START_TAG && parser.getNamespace().equals("jabber:x:data")) { + // Otherwise, it must be a packet extension. + search = new UserSearch(); + search.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(), + parser.getNamespace(), parser)); + + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + + if (search != null) { + return search; + } + return simpleUserSearch; + } + } + + private static void buildDataForm(SimpleUserSearch search, String instructions, XmlPullParser parser) throws Exception { + DataForm dataForm = new DataForm(Form.TYPE_FORM); + boolean done = false; + dataForm.setTitle("User Search"); + dataForm.addInstruction(instructions); + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG && !parser.getNamespace().equals("jabber:x:data")) { + String name = parser.getName(); + FormField field = new FormField(name); + + // Handle hard coded values. + if(name.equals("first")){ + field.setLabel("First Name"); + } + else if(name.equals("last")){ + field.setLabel("Last Name"); + } + else if(name.equals("email")){ + field.setLabel("Email Address"); + } + else if(name.equals("nick")){ + field.setLabel("Nickname"); + } + + field.setType(FormField.TYPE_TEXT_SINGLE); + dataForm.addField(field); + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + else if (eventType == XmlPullParser.START_TAG && parser.getNamespace().equals("jabber:x:data")) { + search.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(), + parser.getNamespace(), parser)); + done = true; + } + } + if (search.getExtension("x", "jabber:x:data") == null) { + search.addExtension(dataForm); + } + } + + +} diff --git a/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearchManager.java b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearchManager.java new file mode 100644 index 000000000..c771d6925 --- /dev/null +++ b/protocols/bundles/org.jivesoftware.smack/src/org/jivesoftware/smackx/search/UserSearchManager.java @@ -0,0 +1,109 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright (C) 1999-2005 Jive Software. All rights reserved. + * + * This software is the proprietary information of Jive Software. + * Use is subject to license terms. + */ +package org.jivesoftware.smackx.search; + +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.ReportedData; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverItems; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +/** + * The UserSearchManager is a facade built upon Jabber Search Services (JEP-055) to allow for searching + * repositories on a Jabber Server. This implementation allows for transparency of implementation of + * searching (DataForms or No DataForms), but allows the user to simply use the DataForm model for both + * types of support. + * <pre> + * XMPPConnection con = new XMPPConnection("jabber.org"); + * con.login("john", "doe"); + * UserSearchManager search = new UserSearchManager(con, "users.jabber.org"); + * Form searchForm = search.getSearchForm(); + * Form answerForm = searchForm.createAnswerForm(); + * answerForm.setAnswer("last", "DeMoro"); + * ReportedData data = search.getSearchResults(answerForm); + * // Use Returned Data + * </pre> + * + * @author Derek DeMoro + */ +public class UserSearchManager { + + private XMPPConnection con; + private UserSearch userSearch; + + /** + * Creates a new UserSearchManager. + * + * @param con the XMPPConnection to use. + */ + public UserSearchManager(XMPPConnection con) { + this.con = con; + userSearch = new UserSearch(); + } + + /** + * Returns the form to fill out to perform a search. + * + * @param searchService the search service to query. + * @return the form to fill out to perform a search. + * @throws XMPPException thrown if a server error has occurred. + */ + public Form getSearchForm(String searchService) throws XMPPException { + return userSearch.getSearchForm(con, searchService); + } + + /** + * Submits a search form to the server and returns the resulting information + * in the form of <code>ReportedData</code> + * + * @param searchForm the <code>Form</code> to submit for searching. + * @param searchService the name of the search service to use. + * @return the ReportedData returned by the server. + * @throws XMPPException thrown if a server error has occurred. + */ + public ReportedData getSearchResults(Form searchForm, String searchService) throws XMPPException { + return userSearch.sendSearchForm(con, searchForm, searchService); + } + + + /** + * Returns a collection of search services found on the server. + * + * @return a Collection of search services found on the server. + * @throws XMPPException thrown if a server error has occurred. + */ + public Collection getSearchServices() throws XMPPException { + List searchServices = new ArrayList(); + ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(con); + DiscoverItems items = discoManager.discoverItems(con.getServiceName()); + for (Iterator it = items.getItems(); it.hasNext();) { + DiscoverItems.Item item = (DiscoverItems.Item) it.next(); + try { + DiscoverInfo info = discoManager.discoverInfo(item.getEntityID()); + if (info.containsFeature("jabber:iq:search")) { + searchServices.add(item.getEntityID()); + } + } + catch (XMPPException e) { + // No info found. + break; + } + } + return searchServices; + } +} |