aboutsummaryrefslogtreecommitdiff
path: root/src/org/jivesoftware/smack
diff options
context:
space:
mode:
Diffstat (limited to 'src/org/jivesoftware/smack')
-rw-r--r--src/org/jivesoftware/smack/AbstractConnectionListener.java46
-rw-r--r--src/org/jivesoftware/smack/AccountManager.java337
-rw-r--r--src/org/jivesoftware/smack/AndroidConnectionConfiguration.java113
-rw-r--r--src/org/jivesoftware/smack/BOSHConfiguration.java124
-rw-r--r--src/org/jivesoftware/smack/BOSHConnection.java779
-rw-r--r--src/org/jivesoftware/smack/BOSHPacketReader.java169
-rw-r--r--src/org/jivesoftware/smack/Chat.java180
-rw-r--r--src/org/jivesoftware/smack/ChatManager.java284
-rw-r--r--src/org/jivesoftware/smack/ChatManagerListener.java37
-rw-r--r--src/org/jivesoftware/smack/Connection.java920
-rw-r--r--src/org/jivesoftware/smack/Connection.java.orig920
-rw-r--r--src/org/jivesoftware/smack/ConnectionConfiguration.java787
-rw-r--r--src/org/jivesoftware/smack/ConnectionCreationListener.java41
-rw-r--r--src/org/jivesoftware/smack/ConnectionListener.java69
-rw-r--r--src/org/jivesoftware/smack/MessageListener.java30
-rw-r--r--src/org/jivesoftware/smack/NonSASLAuthentication.java143
-rw-r--r--src/org/jivesoftware/smack/OpenTrustManager.java49
-rw-r--r--src/org/jivesoftware/smack/PacketCollector.java160
-rw-r--r--src/org/jivesoftware/smack/PacketInterceptor.java49
-rw-r--r--src/org/jivesoftware/smack/PacketListener.java48
-rw-r--r--src/org/jivesoftware/smack/PacketReader.java429
-rw-r--r--src/org/jivesoftware/smack/PacketWriter.java240
-rw-r--r--src/org/jivesoftware/smack/PrivacyList.java74
-rw-r--r--src/org/jivesoftware/smack/PrivacyListListener.java51
-rw-r--r--src/org/jivesoftware/smack/PrivacyListManager.java467
-rw-r--r--src/org/jivesoftware/smack/ReconnectionManager.java227
-rw-r--r--src/org/jivesoftware/smack/Roster.java1038
-rw-r--r--src/org/jivesoftware/smack/RosterEntry.java244
-rw-r--r--src/org/jivesoftware/smack/RosterGroup.java253
-rw-r--r--src/org/jivesoftware/smack/RosterListener.java83
-rw-r--r--src/org/jivesoftware/smack/RosterStorage.java54
-rw-r--r--src/org/jivesoftware/smack/SASLAuthentication.java586
-rw-r--r--src/org/jivesoftware/smack/SASLAuthentication.java.orig586
-rw-r--r--src/org/jivesoftware/smack/ServerTrustManager.java331
-rw-r--r--src/org/jivesoftware/smack/SmackAndroid.java59
-rw-r--r--src/org/jivesoftware/smack/SmackConfiguration.java371
-rw-r--r--src/org/jivesoftware/smack/UserAuthentication.java79
-rw-r--r--src/org/jivesoftware/smack/XMPPConnection.java1116
-rw-r--r--src/org/jivesoftware/smack/XMPPException.java219
-rw-r--r--src/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java126
-rw-r--r--src/org/jivesoftware/smack/compression/JzlibInputOutputStream.java75
-rw-r--r--src/org/jivesoftware/smack/compression/XMPPInputOutputStream.java33
-rw-r--r--src/org/jivesoftware/smack/debugger/ConsoleDebugger.java198
-rw-r--r--src/org/jivesoftware/smack/debugger/SmackDebugger.java98
-rw-r--r--src/org/jivesoftware/smack/debugger/package.html1
-rw-r--r--src/org/jivesoftware/smack/filter/AndFilter.java91
-rw-r--r--src/org/jivesoftware/smack/filter/FromContainsFilter.java54
-rw-r--r--src/org/jivesoftware/smack/filter/FromMatchesFilter.java75
-rw-r--r--src/org/jivesoftware/smack/filter/IQTypeFilter.java48
-rw-r--r--src/org/jivesoftware/smack/filter/MessageTypeFilter.java54
-rw-r--r--src/org/jivesoftware/smack/filter/NotFilter.java50
-rw-r--r--src/org/jivesoftware/smack/filter/OrFilter.java103
-rw-r--r--src/org/jivesoftware/smack/filter/PacketExtensionFilter.java61
-rw-r--r--src/org/jivesoftware/smack/filter/PacketFilter.java63
-rw-r--r--src/org/jivesoftware/smack/filter/PacketIDFilter.java53
-rw-r--r--src/org/jivesoftware/smack/filter/PacketTypeFilter.java61
-rw-r--r--src/org/jivesoftware/smack/filter/ThreadFilter.java50
-rw-r--r--src/org/jivesoftware/smack/filter/ToContainsFilter.java55
-rw-r--r--src/org/jivesoftware/smack/filter/package.html1
-rw-r--r--src/org/jivesoftware/smack/package.html1
-rw-r--r--src/org/jivesoftware/smack/packet/Authentication.java186
-rw-r--r--src/org/jivesoftware/smack/packet/Bind.java71
-rw-r--r--src/org/jivesoftware/smack/packet/DefaultPacketExtension.java133
-rw-r--r--src/org/jivesoftware/smack/packet/IQ.java244
-rw-r--r--src/org/jivesoftware/smack/packet/Message.java672
-rw-r--r--src/org/jivesoftware/smack/packet/Packet.java509
-rw-r--r--src/org/jivesoftware/smack/packet/PacketExtension.java56
-rw-r--r--src/org/jivesoftware/smack/packet/Presence.java358
-rw-r--r--src/org/jivesoftware/smack/packet/Privacy.java323
-rw-r--r--src/org/jivesoftware/smack/packet/PrivacyItem.java462
-rw-r--r--src/org/jivesoftware/smack/packet/Registration.java155
-rw-r--r--src/org/jivesoftware/smack/packet/RosterPacket.java311
-rw-r--r--src/org/jivesoftware/smack/packet/Session.java45
-rw-r--r--src/org/jivesoftware/smack/packet/StreamError.java106
-rw-r--r--src/org/jivesoftware/smack/packet/XMPPError.java453
-rw-r--r--src/org/jivesoftware/smack/packet/package.html1
-rw-r--r--src/org/jivesoftware/smack/provider/EmbeddedExtensionProvider.java109
-rw-r--r--src/org/jivesoftware/smack/provider/IQProvider.java47
-rw-r--r--src/org/jivesoftware/smack/provider/PacketExtensionProvider.java46
-rw-r--r--src/org/jivesoftware/smack/provider/PrivacyProvider.java151
-rw-r--r--src/org/jivesoftware/smack/provider/ProviderManager.java438
-rw-r--r--src/org/jivesoftware/smack/provider/package.html1
-rw-r--r--src/org/jivesoftware/smack/proxy/DirectSocketFactory.java74
-rw-r--r--src/org/jivesoftware/smack/proxy/HTTPProxySocketFactory.java172
-rw-r--r--src/org/jivesoftware/smack/proxy/ProxyException.java44
-rw-r--r--src/org/jivesoftware/smack/proxy/ProxyInfo.java131
-rw-r--r--src/org/jivesoftware/smack/proxy/Socks4ProxySocketFactory.java216
-rw-r--r--src/org/jivesoftware/smack/proxy/Socks5ProxySocketFactory.java375
-rw-r--r--src/org/jivesoftware/smack/sasl/SASLAnonymous.java62
-rw-r--r--src/org/jivesoftware/smack/sasl/SASLCramMD5Mechanism.java38
-rw-r--r--src/org/jivesoftware/smack/sasl/SASLDigestMD5Mechanism.java38
-rw-r--r--src/org/jivesoftware/smack/sasl/SASLExternalMechanism.java59
-rw-r--r--src/org/jivesoftware/smack/sasl/SASLFacebookConnect.java201
-rw-r--r--src/org/jivesoftware/smack/sasl/SASLGSSAPIMechanism.java89
-rw-r--r--src/org/jivesoftware/smack/sasl/SASLMechanism.java323
-rw-r--r--src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java34
-rw-r--r--src/org/jivesoftware/smack/sasl/package.html1
-rw-r--r--src/org/jivesoftware/smack/util/Base32Encoder.java184
-rw-r--r--src/org/jivesoftware/smack/util/Base64.java1689
-rw-r--r--src/org/jivesoftware/smack/util/Base64Encoder.java42
-rw-r--r--src/org/jivesoftware/smack/util/Base64FileUrlEncoder.java48
-rw-r--r--src/org/jivesoftware/smack/util/Cache.java678
-rw-r--r--src/org/jivesoftware/smack/util/DNSUtil.java229
-rw-r--r--src/org/jivesoftware/smack/util/DateFormatType.java65
-rw-r--r--src/org/jivesoftware/smack/util/ObservableReader.java118
-rw-r--r--src/org/jivesoftware/smack/util/ObservableWriter.java120
-rw-r--r--src/org/jivesoftware/smack/util/PacketParserUtils.java925
-rw-r--r--src/org/jivesoftware/smack/util/PacketParserUtils.java.orig926
-rw-r--r--src/org/jivesoftware/smack/util/ReaderListener.java41
-rw-r--r--src/org/jivesoftware/smack/util/StringEncoder.java36
-rw-r--r--src/org/jivesoftware/smack/util/StringUtils.java800
-rw-r--r--src/org/jivesoftware/smack/util/SyncPacketSend.java63
-rw-r--r--src/org/jivesoftware/smack/util/WriterListener.java41
-rw-r--r--src/org/jivesoftware/smack/util/collections/AbstractEmptyIterator.java89
-rw-r--r--src/org/jivesoftware/smack/util/collections/AbstractHashedMap.java1338
-rw-r--r--src/org/jivesoftware/smack/util/collections/AbstractKeyValue.java80
-rw-r--r--src/org/jivesoftware/smack/util/collections/AbstractMapEntry.java89
-rw-r--r--src/org/jivesoftware/smack/util/collections/AbstractReferenceMap.java1025
-rw-r--r--src/org/jivesoftware/smack/util/collections/DefaultMapEntry.java65
-rw-r--r--src/org/jivesoftware/smack/util/collections/EmptyIterator.java58
-rw-r--r--src/org/jivesoftware/smack/util/collections/EmptyMapIterator.java42
-rw-r--r--src/org/jivesoftware/smack/util/collections/IterableMap.java61
-rw-r--r--src/org/jivesoftware/smack/util/collections/KeyValue.java46
-rw-r--r--src/org/jivesoftware/smack/util/collections/MapIterator.java109
-rw-r--r--src/org/jivesoftware/smack/util/collections/ReferenceMap.java161
-rw-r--r--src/org/jivesoftware/smack/util/collections/ResettableIterator.java38
-rw-r--r--src/org/jivesoftware/smack/util/dns/DNSJavaResolver.java73
-rw-r--r--src/org/jivesoftware/smack/util/dns/DNSResolver.java33
-rw-r--r--src/org/jivesoftware/smack/util/dns/HostAddress.java109
-rw-r--r--src/org/jivesoftware/smack/util/dns/SRVRecord.java79
-rw-r--r--src/org/jivesoftware/smack/util/package.html1
131 files changed, 29378 insertions, 0 deletions
diff --git a/src/org/jivesoftware/smack/AbstractConnectionListener.java b/src/org/jivesoftware/smack/AbstractConnectionListener.java
new file mode 100644
index 0000000..69acf90
--- /dev/null
+++ b/src/org/jivesoftware/smack/AbstractConnectionListener.java
@@ -0,0 +1,46 @@
+/**
+ * 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;
+
+/**
+ * The AbstractConnectionListener class provides an empty implementation for all
+ * methods defined by the {@link ConnectionListener} interface. This is a
+ * convenience class which should be used in case you do not need to implement
+ * all methods.
+ *
+ * @author Henning Staib
+ */
+public class AbstractConnectionListener implements ConnectionListener {
+
+ public void connectionClosed() {
+ // do nothing
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ // do nothing
+ }
+
+ public void reconnectingIn(int seconds) {
+ // do nothing
+ }
+
+ public void reconnectionFailed(Exception e) {
+ // do nothing
+ }
+
+ public void reconnectionSuccessful() {
+ // do nothing
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/AccountManager.java b/src/org/jivesoftware/smack/AccountManager.java
new file mode 100644
index 0000000..4d9faa5
--- /dev/null
+++ b/src/org/jivesoftware/smack/AccountManager.java
@@ -0,0 +1,337 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.AndFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Registration;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Allows creation and management of accounts on an XMPP server.
+ *
+ * @see Connection#getAccountManager()
+ * @author Matt Tucker
+ */
+public class AccountManager {
+
+ private Connection connection;
+ private Registration info = null;
+
+ /**
+ * Flag that indicates whether the server supports In-Band Registration.
+ * In-Band Registration may be advertised as a stream feature. If no stream feature
+ * was advertised from the server then try sending an IQ packet to discover if In-Band
+ * Registration is available.
+ */
+ private boolean accountCreationSupported = false;
+
+ /**
+ * Creates a new AccountManager instance.
+ *
+ * @param connection a connection to a XMPP server.
+ */
+ public AccountManager(Connection connection) {
+ this.connection = connection;
+ }
+
+ /**
+ * Sets whether the server supports In-Band Registration. In-Band Registration may be
+ * advertised as a stream feature. If no stream feature was advertised from the server
+ * then try sending an IQ packet to discover if In-Band Registration is available.
+ *
+ * @param accountCreationSupported true if the server supports In-Band Registration.
+ */
+ void setSupportsAccountCreation(boolean accountCreationSupported) {
+ this.accountCreationSupported = accountCreationSupported;
+ }
+
+ /**
+ * 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() {
+ // Check if we already know that the server supports creating new accounts
+ if (accountCreationSupported) {
+ return true;
+ }
+ // No information is known yet (e.g. no stream feature was received from the server
+ // indicating that it supports creating new accounts) so send an IQ packet as a way
+ // to discover if this feature is supported
+ try {
+ if (info == null) {
+ getRegistrationInfo();
+ accountCreationSupported = info.getType() != IQ.Type.ERROR;
+ }
+ return accountCreationSupported;
+ }
+ catch (XMPPException xe) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns an unmodifiable collection of the names of the required account attributes.
+ * All attributes must be set when creating new accounts. The standard set of possible
+ * 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 Collection<String> getAccountAttributes() {
+ try {
+ if (info == null) {
+ getRegistrationInfo();
+ }
+ Map<String, String> attributes = info.getAttributes();
+ if (attributes != null) {
+ return Collections.unmodifiableSet(attributes.keySet());
+ }
+ }
+ catch (XMPPException xe) {
+ xe.printStackTrace();
+ }
+ return Collections.emptySet();
+ }
+
+ /**
+ * 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 info.getAttributes().get(name);
+ }
+ catch (XMPPException xe) {
+ xe.printStackTrace();
+ }
+ 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<String, String> attributes = new HashMap<String, String>();
+ for (String attributeName : getAccountAttributes()) {
+ 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<String, String> 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());
+ Map<String, String> map = new HashMap<String, String>();
+ 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<String, String> attributes = new HashMap<String, String>();
+ // 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/src/org/jivesoftware/smack/AndroidConnectionConfiguration.java b/src/org/jivesoftware/smack/AndroidConnectionConfiguration.java
new file mode 100644
index 0000000..6ec05e0
--- /dev/null
+++ b/src/org/jivesoftware/smack/AndroidConnectionConfiguration.java
@@ -0,0 +1,113 @@
+package org.jivesoftware.smack;
+
+import java.io.File;
+
+import android.os.Build;
+
+import org.jivesoftware.smack.proxy.ProxyInfo;
+import org.jivesoftware.smack.util.DNSUtil;
+import org.jivesoftware.smack.util.dns.HostAddress;
+
+import java.util.List;
+
+/**
+ * This class wraps DNS SRV lookups for a new ConnectionConfiguration in a
+ * new thread, since Android API >= 11 (Honeycomb) does not allow network
+ * activity in the main thread.
+ *
+ * @author Florian Schmaus fschmaus@gmail.com
+ *
+ */
+public class AndroidConnectionConfiguration extends ConnectionConfiguration {
+ private static final int DEFAULT_TIMEOUT = 10000;
+
+ /**
+ * Creates a new ConnectionConfiguration for the specified service name.
+ * A DNS SRV lookup will be performed to find out the actual host address
+ * and port to use for the connection.
+ *
+ * @param serviceName the name of the service provided by an XMPP server.
+ */
+ public AndroidConnectionConfiguration(String serviceName) throws XMPPException {
+ super();
+ AndroidInit(serviceName, DEFAULT_TIMEOUT);
+ }
+
+ /**
+ *
+ * @param serviceName
+ * @param timeout
+ * @throws XMPPException
+ */
+ public AndroidConnectionConfiguration(String serviceName, int timeout) throws XMPPException {
+ super();
+ AndroidInit(serviceName, timeout);
+ }
+
+ public AndroidConnectionConfiguration(String host, int port, String name) {
+ super(host, port, name);
+ AndroidInit();
+ }
+
+ private void AndroidInit() {
+ // API 14 is Ice Cream Sandwich
+ if (Build.VERSION.SDK_INT >= 14) {
+ setTruststoreType("AndroidCAStore");
+ setTruststorePassword(null);
+ setTruststorePath(null);
+ } else {
+ setTruststoreType("BKS");
+ String path = System.getProperty("javax.net.ssl.trustStore");
+ if (path == null)
+ path = System.getProperty("java.home") + File.separator + "etc"
+ + File.separator + "security" + File.separator
+ + "cacerts.bks";
+ setTruststorePath(path);
+ }
+ }
+
+ /**
+ *
+ * @param serviceName
+ * @param timeout
+ * @throws XMPPException
+ */
+ private void AndroidInit(String serviceName, int timeout) throws XMPPException {
+ AndroidInit();
+ class DnsSrvLookupRunnable implements Runnable {
+ String serviceName;
+ List<HostAddress> addresses;
+
+ public DnsSrvLookupRunnable(String serviceName) {
+ this.serviceName = serviceName;
+ }
+
+ @Override
+ public void run() {
+ addresses = DNSUtil.resolveXMPPDomain(serviceName);
+ }
+
+ public List<HostAddress> getHostAddresses() {
+ return addresses;
+ }
+ }
+
+ DnsSrvLookupRunnable dnsSrv = new DnsSrvLookupRunnable(serviceName);
+ Thread t = new Thread(dnsSrv, "dns-srv-lookup");
+ t.start();
+ try {
+ t.join(timeout);
+ } catch (InterruptedException e) {
+ throw new XMPPException("DNS lookup timeout after " + timeout + "ms", e);
+ }
+
+ hostAddresses = dnsSrv.getHostAddresses();
+ if (hostAddresses == null) {
+ throw new XMPPException("DNS lookup failure");
+ }
+
+ ProxyInfo proxy = ProxyInfo.forDefaultProxy();
+
+ init(serviceName, proxy);
+ }
+}
diff --git a/src/org/jivesoftware/smack/BOSHConfiguration.java b/src/org/jivesoftware/smack/BOSHConfiguration.java
new file mode 100644
index 0000000..0b033b4
--- /dev/null
+++ b/src/org/jivesoftware/smack/BOSHConfiguration.java
@@ -0,0 +1,124 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 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.net.URI;
+import java.net.URISyntaxException;
+
+import org.jivesoftware.smack.ConnectionConfiguration;
+import org.jivesoftware.smack.proxy.ProxyInfo;
+
+/**
+ * Configuration to use while establishing the connection to the XMPP server via
+ * HTTP binding.
+ *
+ * @see BOSHConnection
+ * @author Guenther Niess
+ */
+public class BOSHConfiguration extends ConnectionConfiguration {
+
+ private boolean ssl;
+ private String file;
+
+ public BOSHConfiguration(String xmppDomain) {
+ super(xmppDomain, 7070);
+ setSASLAuthenticationEnabled(true);
+ ssl = false;
+ file = "/http-bind/";
+ }
+
+ public BOSHConfiguration(String xmppDomain, int port) {
+ super(xmppDomain, port);
+ setSASLAuthenticationEnabled(true);
+ ssl = false;
+ file = "/http-bind/";
+ }
+
+ /**
+ * Create a HTTP Binding configuration.
+ *
+ * @param https true if you want to use SSL
+ * (e.g. false for http://domain.lt:7070/http-bind).
+ * @param host the hostname or IP address of the connection manager
+ * (e.g. domain.lt for http://domain.lt:7070/http-bind).
+ * @param port the port of the connection manager
+ * (e.g. 7070 for http://domain.lt:7070/http-bind).
+ * @param filePath the file which is described by the URL
+ * (e.g. /http-bind for http://domain.lt:7070/http-bind).
+ * @param xmppDomain the XMPP service name
+ * (e.g. domain.lt for the user alice@domain.lt)
+ */
+ public BOSHConfiguration(boolean https, String host, int port, String filePath, String xmppDomain) {
+ super(host, port, xmppDomain);
+ setSASLAuthenticationEnabled(true);
+ ssl = https;
+ file = (filePath != null ? filePath : "/");
+ }
+
+ /**
+ * Create a HTTP Binding configuration.
+ *
+ * @param https true if you want to use SSL
+ * (e.g. false for http://domain.lt:7070/http-bind).
+ * @param host the hostname or IP address of the connection manager
+ * (e.g. domain.lt for http://domain.lt:7070/http-bind).
+ * @param port the port of the connection manager
+ * (e.g. 7070 for http://domain.lt:7070/http-bind).
+ * @param filePath the file which is described by the URL
+ * (e.g. /http-bind for http://domain.lt:7070/http-bind).
+ * @param proxy the configuration of a proxy server.
+ * @param xmppDomain the XMPP service name
+ * (e.g. domain.lt for the user alice@domain.lt)
+ */
+ public BOSHConfiguration(boolean https, String host, int port, String filePath, ProxyInfo proxy, String xmppDomain) {
+ super(host, port, xmppDomain, proxy);
+ setSASLAuthenticationEnabled(true);
+ ssl = https;
+ file = (filePath != null ? filePath : "/");
+ }
+
+ public boolean isProxyEnabled() {
+ return (proxy != null && proxy.getProxyType() != ProxyInfo.ProxyType.NONE);
+ }
+
+ public ProxyInfo getProxyInfo() {
+ return proxy;
+ }
+
+ public String getProxyAddress() {
+ return (proxy != null ? proxy.getProxyAddress() : null);
+ }
+
+ public int getProxyPort() {
+ return (proxy != null ? proxy.getProxyPort() : 8080);
+ }
+
+ public boolean isUsingSSL() {
+ return ssl;
+ }
+
+ public URI getURI() throws URISyntaxException {
+ if (file.charAt(0) != '/') {
+ file = '/' + file;
+ }
+ return new URI((ssl ? "https://" : "http://") + getHost() + ":" + getPort() + file);
+ }
+}
diff --git a/src/org/jivesoftware/smack/BOSHConnection.java b/src/org/jivesoftware/smack/BOSHConnection.java
new file mode 100644
index 0000000..594cf9d
--- /dev/null
+++ b/src/org/jivesoftware/smack/BOSHConnection.java
@@ -0,0 +1,779 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 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.IOException;
+import java.io.PipedReader;
+import java.io.PipedWriter;
+import java.io.Writer;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.Roster;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+
+import com.kenai.jbosh.BOSHClient;
+import com.kenai.jbosh.BOSHClientConfig;
+import com.kenai.jbosh.BOSHClientConnEvent;
+import com.kenai.jbosh.BOSHClientConnListener;
+import com.kenai.jbosh.BOSHClientRequestListener;
+import com.kenai.jbosh.BOSHClientResponseListener;
+import com.kenai.jbosh.BOSHException;
+import com.kenai.jbosh.BOSHMessageEvent;
+import com.kenai.jbosh.BodyQName;
+import com.kenai.jbosh.ComposableBody;
+
+/**
+ * Creates a connection to a XMPP server via HTTP binding.
+ * This is specified in the XEP-0206: XMPP Over BOSH.
+ *
+ * @see Connection
+ * @author Guenther Niess
+ */
+public class BOSHConnection extends Connection {
+
+ /**
+ * The XMPP Over Bosh namespace.
+ */
+ public static final String XMPP_BOSH_NS = "urn:xmpp:xbosh";
+
+ /**
+ * The BOSH namespace from XEP-0124.
+ */
+ public static final String BOSH_URI = "http://jabber.org/protocol/httpbind";
+
+ /**
+ * The used BOSH client from the jbosh library.
+ */
+ private BOSHClient client;
+
+ /**
+ * Holds the initial configuration used while creating the connection.
+ */
+ private final BOSHConfiguration config;
+
+ // Some flags which provides some info about the current state.
+ private boolean connected = false;
+ private boolean authenticated = false;
+ private boolean anonymous = false;
+ private boolean isFirstInitialization = true;
+ private boolean wasAuthenticated = false;
+ private boolean done = false;
+
+ /**
+ * The Thread environment for sending packet listeners.
+ */
+ private ExecutorService listenerExecutor;
+
+ // The readerPipe and consumer thread are used for the debugger.
+ private PipedWriter readerPipe;
+ private Thread readerConsumer;
+
+ /**
+ * The BOSH equivalent of the stream ID which is used for DIGEST authentication.
+ */
+ protected String authID = null;
+
+ /**
+ * The session ID for the BOSH session with the connection manager.
+ */
+ protected String sessionID = null;
+
+ /**
+ * The full JID of the authenticated user.
+ */
+ private String user = null;
+
+ /**
+ * The roster maybe also called buddy list holds the list of the users contacts.
+ */
+ private Roster roster = null;
+
+
+ /**
+ * Create a HTTP Binding connection to a XMPP server.
+ *
+ * @param https true if you want to use SSL
+ * (e.g. false for http://domain.lt:7070/http-bind).
+ * @param host the hostname or IP address of the connection manager
+ * (e.g. domain.lt for http://domain.lt:7070/http-bind).
+ * @param port the port of the connection manager
+ * (e.g. 7070 for http://domain.lt:7070/http-bind).
+ * @param filePath the file which is described by the URL
+ * (e.g. /http-bind for http://domain.lt:7070/http-bind).
+ * @param xmppDomain the XMPP service name
+ * (e.g. domain.lt for the user alice@domain.lt)
+ */
+ public BOSHConnection(boolean https, String host, int port, String filePath, String xmppDomain) {
+ super(new BOSHConfiguration(https, host, port, filePath, xmppDomain));
+ this.config = (BOSHConfiguration) getConfiguration();
+ }
+
+ /**
+ * Create a HTTP Binding connection to a XMPP server.
+ *
+ * @param config The configuration which is used for this connection.
+ */
+ public BOSHConnection(BOSHConfiguration config) {
+ super(config);
+ this.config = config;
+ }
+
+ public void connect() throws XMPPException {
+ if (connected) {
+ throw new IllegalStateException("Already connected to a server.");
+ }
+ done = false;
+ try {
+ // Ensure a clean starting state
+ if (client != null) {
+ client.close();
+ client = null;
+ }
+ saslAuthentication.init();
+ sessionID = null;
+ authID = null;
+
+ // Initialize BOSH client
+ BOSHClientConfig.Builder cfgBuilder = BOSHClientConfig.Builder
+ .create(config.getURI(), config.getServiceName());
+ if (config.isProxyEnabled()) {
+ cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort());
+ }
+ client = BOSHClient.create(cfgBuilder.build());
+
+ // Create an executor to deliver incoming packets to listeners.
+ // We'll use a single thread with an unbounded queue.
+ listenerExecutor = Executors
+ .newSingleThreadExecutor(new ThreadFactory() {
+ public Thread newThread(Runnable runnable) {
+ Thread thread = new Thread(runnable,
+ "Smack Listener Processor ("
+ + connectionCounterValue + ")");
+ thread.setDaemon(true);
+ return thread;
+ }
+ });
+ client.addBOSHClientConnListener(new BOSHConnectionListener(this));
+ client.addBOSHClientResponseListener(new BOSHPacketReader(this));
+
+ // Initialize the debugger
+ if (config.isDebuggerEnabled()) {
+ initDebugger();
+ if (isFirstInitialization) {
+ if (debugger.getReaderListener() != null) {
+ addPacketListener(debugger.getReaderListener(), null);
+ }
+ if (debugger.getWriterListener() != null) {
+ addPacketSendingListener(debugger.getWriterListener(), null);
+ }
+ }
+ }
+
+ // Send the session creation request
+ client.send(ComposableBody.builder()
+ .setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
+ .setAttribute(BodyQName.createWithPrefix(XMPP_BOSH_NS, "version", "xmpp"), "1.0")
+ .build());
+ } catch (Exception e) {
+ throw new XMPPException("Can't connect to " + getServiceName(), e);
+ }
+
+ // Wait for the response from the server
+ synchronized (this) {
+ long endTime = System.currentTimeMillis() +
+ SmackConfiguration.getPacketReplyTimeout() * 6;
+ while ((!connected) && (System.currentTimeMillis() < endTime)) {
+ try {
+ wait(Math.abs(endTime - System.currentTimeMillis()));
+ }
+ catch (InterruptedException e) {}
+ }
+ }
+
+ // If there is no feedback, throw an remote server timeout error
+ if (!connected && !done) {
+ done = true;
+ String errorMessage = "Timeout reached for the connection to "
+ + getHost() + ":" + getPort() + ".";
+ throw new XMPPException(
+ errorMessage,
+ new XMPPError(XMPPError.Condition.remote_server_timeout, errorMessage));
+ }
+ }
+
+ public String getConnectionID() {
+ if (!connected) {
+ return null;
+ } else if (authID != null) {
+ return authID;
+ } else {
+ return sessionID;
+ }
+ }
+
+ public Roster getRoster() {
+ if (roster == null) {
+ return null;
+ }
+ if (!config.isRosterLoadedAtLogin()) {
+ roster.reload();
+ }
+ // 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;
+ }
+
+ public String getUser() {
+ return user;
+ }
+
+ public boolean isAnonymous() {
+ return anonymous;
+ }
+
+ public boolean isAuthenticated() {
+ return authenticated;
+ }
+
+ public boolean isConnected() {
+ return connected;
+ }
+
+ public boolean isSecureConnection() {
+ // TODO: Implement SSL usage
+ return false;
+ }
+
+ public boolean isUsingCompression() {
+ // TODO: Implement compression
+ return false;
+ }
+
+ public void login(String username, String password, String resource)
+ 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;
+ if (config.isSASLAuthenticationEnabled()
+ && saslAuthentication.hasNonAnonymousAuthentication()) {
+ // Authenticate using SASL
+ if (password != null) {
+ response = saslAuthentication.authenticate(username, password, resource);
+ } else {
+ response = saslAuthentication.authenticate(username, resource, config.getCallbackHandler());
+ }
+ } 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
+ config.setServiceName(StringUtils.parseServer(response));
+ } else {
+ this.user = username + "@" + getServiceName();
+ if (resource != null) {
+ this.user += "/" + resource;
+ }
+ }
+
+ // Create the roster if it is not a reconnection.
+ if (this.roster == null) {
+ if (this.rosterStorage == null) {
+ this.roster = new Roster(this);
+ } else {
+ this.roster = new Roster(this, rosterStorage);
+ }
+ }
+
+ // Set presence to online.
+ if (config.isSendPresence()) {
+ sendPacket(new Presence(Presence.Type.available));
+ }
+
+ // Indicate that we're now authenticated.
+ authenticated = true;
+ anonymous = false;
+
+ if (config.isRosterLoadedAtLogin()) {
+ this.roster.reload();
+ }
+ // Stores the autentication for future reconnection
+ config.setLoginInfo(username, password, resource);
+
+ // If debugging is enabled, change the the debug window title to include
+ // the
+ // name we are now logged-in as.l
+ if (config.isDebuggerEnabled() && debugger != null) {
+ debugger.userHasLogged(user);
+ }
+ }
+
+ public 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;
+ if (config.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
+ config.setServiceName(StringUtils.parseServer(response));
+
+ // Anonymous users can't have a roster.
+ roster = null;
+
+ // Set presence to online.
+ if (config.isSendPresence()) {
+ sendPacket(new Presence(Presence.Type.available));
+ }
+
+ // Indicate that we're now authenticated.
+ authenticated = true;
+ 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 (config.isDebuggerEnabled() && debugger != null) {
+ debugger.userHasLogged(user);
+ }
+ }
+
+ public void sendPacket(Packet packet) {
+ if (!isConnected()) {
+ throw new IllegalStateException("Not connected to server.");
+ }
+ if (packet == null) {
+ throw new NullPointerException("Packet is null.");
+ }
+ if (!done) {
+ // Invoke interceptors for the new packet that is about to be sent.
+ // Interceptors
+ // may modify the content of the packet.
+ firePacketInterceptors(packet);
+
+ try {
+ send(ComposableBody.builder().setPayloadXML(packet.toXML())
+ .build());
+ } catch (BOSHException e) {
+ e.printStackTrace();
+ return;
+ }
+
+ // Process packet writer listeners. Note that we're using the
+ // sending
+ // thread so it's expected that listeners are fast.
+ firePacketSendingListeners(packet);
+ }
+ }
+
+ public void disconnect(Presence unavailablePresence) {
+ if (!connected) {
+ return;
+ }
+ shutdown(unavailablePresence);
+
+ // Cleanup
+ if (roster != null) {
+ roster.cleanup();
+ roster = null;
+ }
+ sendListeners.clear();
+ recvListeners.clear();
+ collectors.clear();
+ interceptors.clear();
+
+ // Reset the connection flags
+ wasAuthenticated = false;
+ isFirstInitialization = true;
+
+ // Notify connection listeners of the connection closing if done hasn't already been set.
+ for (ConnectionListener listener : getConnectionListeners()) {
+ try {
+ listener.connectionClosed();
+ }
+ catch (Exception e) {
+ // Catch and print any exception so we can recover
+ // from a faulty listener and finish the shutdown process
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Closes the connection by setting presence to unavailable and closing the
+ * HTTP client. The shutdown logic will be used during a planned disconnection or when
+ * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's
+ * BOSH packet reader and {@link Roster} will not be removed; thus
+ * connection's state is kept.
+ *
+ * @param unavailablePresence the presence packet to send during shutdown.
+ */
+ protected void shutdown(Presence unavailablePresence) {
+ setWasAuthenticated(authenticated);
+ authID = null;
+ sessionID = null;
+ done = true;
+ authenticated = false;
+ connected = false;
+ isFirstInitialization = false;
+
+ try {
+ client.disconnect(ComposableBody.builder()
+ .setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
+ .setPayloadXML(unavailablePresence.toXML())
+ .build());
+ // Wait 150 ms for processes to clean-up, then shutdown.
+ Thread.sleep(150);
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+
+ // Close down the readers and writers.
+ if (readerPipe != null) {
+ try {
+ readerPipe.close();
+ }
+ catch (Throwable ignore) { /* ignore */ }
+ reader = 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;
+ }
+
+ // Shut down the listener executor.
+ if (listenerExecutor != null) {
+ listenerExecutor.shutdown();
+ }
+ readerConsumer = null;
+ }
+
+ /**
+ * Sets whether the connection has already logged in the server.
+ *
+ * @param wasAuthenticated true if the connection has already been authenticated.
+ */
+ private void setWasAuthenticated(boolean wasAuthenticated) {
+ if (!this.wasAuthenticated) {
+ this.wasAuthenticated = wasAuthenticated;
+ }
+ }
+
+ /**
+ * Send a HTTP request to the connection manager with the provided body element.
+ *
+ * @param body the body which will be sent.
+ */
+ protected void send(ComposableBody body) throws BOSHException {
+ if (!connected) {
+ throw new IllegalStateException("Not connected to a server!");
+ }
+ if (body == null) {
+ throw new NullPointerException("Body mustn't be null!");
+ }
+ if (sessionID != null) {
+ body = body.rebuild().setAttribute(
+ BodyQName.create(BOSH_URI, "sid"), sessionID).build();
+ }
+ client.send(body);
+ }
+
+ /**
+ * 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.
+ */
+ protected void processPacket(Packet packet) {
+ if (packet == null) {
+ return;
+ }
+
+ // Loop through all collectors and notify the appropriate ones.
+ for (PacketCollector collector : getPacketCollectors()) {
+ collector.processPacket(packet);
+ }
+
+ // Deliver the incoming packet to listeners.
+ listenerExecutor.submit(new ListenerNotification(packet));
+ }
+
+ /**
+ * Initialize the SmackDebugger which allows to log and debug XML traffic.
+ */
+ protected void initDebugger() {
+ // TODO: Maybe we want to extend the SmackDebugger for simplification
+ // and a performance boost.
+
+ // Initialize a empty writer which discards all data.
+ writer = new Writer() {
+ public void write(char[] cbuf, int off, int len) { /* ignore */}
+ public void close() { /* ignore */ }
+ public void flush() { /* ignore */ }
+ };
+
+ // Initialize a pipe for received raw data.
+ try {
+ readerPipe = new PipedWriter();
+ reader = new PipedReader(readerPipe);
+ }
+ catch (IOException e) {
+ // Ignore
+ }
+
+ // Call the method from the parent class which initializes the debugger.
+ super.initDebugger();
+
+ // Add listeners for the received and sent raw data.
+ client.addBOSHClientResponseListener(new BOSHClientResponseListener() {
+ public void responseReceived(BOSHMessageEvent event) {
+ if (event.getBody() != null) {
+ try {
+ readerPipe.write(event.getBody().toXML());
+ readerPipe.flush();
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ }
+ });
+ client.addBOSHClientRequestListener(new BOSHClientRequestListener() {
+ public void requestSent(BOSHMessageEvent event) {
+ if (event.getBody() != null) {
+ try {
+ writer.write(event.getBody().toXML());
+ } catch (Exception e) {
+ // Ignore
+ }
+ }
+ }
+ });
+
+ // Create and start a thread which discards all read data.
+ readerConsumer = new Thread() {
+ private Thread thread = this;
+ private int bufferLength = 1024;
+
+ public void run() {
+ try {
+ char[] cbuf = new char[bufferLength];
+ while (readerConsumer == thread && !done) {
+ reader.read(cbuf, 0, bufferLength);
+ }
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ };
+ readerConsumer.setDaemon(true);
+ readerConsumer.start();
+ }
+
+ /**
+ * 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.
+ */
+ protected void notifyConnectionError(Exception e) {
+ // Closes the connection temporary. A reconnection is possible
+ shutdown(new Presence(Presence.Type.unavailable));
+ // Print the stack trace to help catch the problem
+ e.printStackTrace();
+ // Notify connection listeners of the error.
+ for (ConnectionListener listener : getConnectionListeners()) {
+ try {
+ listener.connectionClosedOnError(e);
+ }
+ catch (Exception e2) {
+ // Catch and print any exception so we can recover
+ // from a faulty listener
+ e2.printStackTrace();
+ }
+ }
+ }
+
+
+ /**
+ * A listener class which listen for a successfully established connection
+ * and connection errors and notifies the BOSHConnection.
+ *
+ * @author Guenther Niess
+ */
+ private class BOSHConnectionListener implements BOSHClientConnListener {
+
+ private final BOSHConnection connection;
+
+ public BOSHConnectionListener(BOSHConnection connection) {
+ this.connection = connection;
+ }
+
+ /**
+ * Notify the BOSHConnection about connection state changes.
+ * Process the connection listeners and try to login if the
+ * connection was formerly authenticated and is now reconnected.
+ */
+ public void connectionEvent(BOSHClientConnEvent connEvent) {
+ try {
+ if (connEvent.isConnected()) {
+ connected = true;
+ if (isFirstInitialization) {
+ isFirstInitialization = false;
+ for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
+ listener.connectionCreated(connection);
+ }
+ }
+ else {
+ try {
+ if (wasAuthenticated) {
+ connection.login(
+ config.getUsername(),
+ config.getPassword(),
+ config.getResource());
+ }
+ for (ConnectionListener listener : getConnectionListeners()) {
+ listener.reconnectionSuccessful();
+ }
+ }
+ catch (XMPPException e) {
+ for (ConnectionListener listener : getConnectionListeners()) {
+ listener.reconnectionFailed(e);
+ }
+ }
+ }
+ }
+ else {
+ if (connEvent.isError()) {
+ try {
+ connEvent.getCause();
+ }
+ catch (Exception e) {
+ notifyConnectionError(e);
+ }
+ }
+ connected = false;
+ }
+ }
+ finally {
+ synchronized (connection) {
+ connection.notifyAll();
+ }
+ }
+ }
+ }
+
+ /**
+ * This class notifies all listeners that a packet was received.
+ */
+ private class ListenerNotification implements Runnable {
+
+ private Packet packet;
+
+ public ListenerNotification(Packet packet) {
+ this.packet = packet;
+ }
+
+ public void run() {
+ for (ListenerWrapper listenerWrapper : recvListeners.values()) {
+ listenerWrapper.notifyListener(packet);
+ }
+ }
+ }
+
+ @Override
+ public void setRosterStorage(RosterStorage storage)
+ throws IllegalStateException {
+ if(this.roster!=null){
+ throw new IllegalStateException("Roster is already initialized");
+ }
+ this.rosterStorage = storage;
+ }
+}
diff --git a/src/org/jivesoftware/smack/BOSHPacketReader.java b/src/org/jivesoftware/smack/BOSHPacketReader.java
new file mode 100644
index 0000000..c86d756
--- /dev/null
+++ b/src/org/jivesoftware/smack/BOSHPacketReader.java
@@ -0,0 +1,169 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 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.StringReader;
+
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smack.sasl.SASLMechanism.Challenge;
+import org.jivesoftware.smack.sasl.SASLMechanism.Failure;
+import org.jivesoftware.smack.sasl.SASLMechanism.Success;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlPullParser;
+
+import com.kenai.jbosh.AbstractBody;
+import com.kenai.jbosh.BOSHClientResponseListener;
+import com.kenai.jbosh.BOSHMessageEvent;
+import com.kenai.jbosh.BodyQName;
+import com.kenai.jbosh.ComposableBody;
+
+/**
+ * Listens for XML traffic from the BOSH connection manager and parses it into
+ * packet objects.
+ *
+ * @author Guenther Niess
+ */
+public class BOSHPacketReader implements BOSHClientResponseListener {
+
+ private BOSHConnection connection;
+
+ /**
+ * Create a packet reader which listen on a BOSHConnection for received
+ * HTTP responses, parse the packets and notifies the connection.
+ *
+ * @param connection the corresponding connection for the received packets.
+ */
+ public BOSHPacketReader(BOSHConnection connection) {
+ this.connection = connection;
+ }
+
+ /**
+ * Parse the received packets and notify the corresponding connection.
+ *
+ * @param event the BOSH client response which includes the received packet.
+ */
+ public void responseReceived(BOSHMessageEvent event) {
+ AbstractBody body = event.getBody();
+ if (body != null) {
+ try {
+ if (connection.sessionID == null) {
+ connection.sessionID = body.getAttribute(BodyQName.create(BOSHConnection.BOSH_URI, "sid"));
+ }
+ if (connection.authID == null) {
+ connection.authID = body.getAttribute(BodyQName.create(BOSHConnection.BOSH_URI, "authid"));
+ }
+ final XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES,
+ true);
+ parser.setInput(new StringReader(body.toXML()));
+ int eventType = parser.getEventType();
+ do {
+ eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("body")) {
+ // ignore the container root element
+ } else if (parser.getName().equals("message")) {
+ connection.processPacket(PacketParserUtils.parseMessage(parser));
+ } else if (parser.getName().equals("iq")) {
+ connection.processPacket(PacketParserUtils.parseIQ(parser, connection));
+ } else if (parser.getName().equals("presence")) {
+ connection.processPacket(PacketParserUtils.parsePresence(parser));
+ } else if (parser.getName().equals("challenge")) {
+ // The server is challenging the SASL authentication
+ // made by the client
+ final String challengeData = parser.nextText();
+ connection.getSASLAuthentication()
+ .challengeReceived(challengeData);
+ connection.processPacket(new Challenge(
+ challengeData));
+ } else if (parser.getName().equals("success")) {
+ connection.send(ComposableBody.builder()
+ .setNamespaceDefinition("xmpp", BOSHConnection.XMPP_BOSH_NS)
+ .setAttribute(
+ BodyQName.createWithPrefix(BOSHConnection.XMPP_BOSH_NS, "restart", "xmpp"),
+ "true")
+ .setAttribute(
+ BodyQName.create(BOSHConnection.BOSH_URI, "to"),
+ connection.getServiceName())
+ .build());
+ connection.getSASLAuthentication().authenticated();
+ connection.processPacket(new Success(parser.nextText()));
+ } else if (parser.getName().equals("features")) {
+ parseFeatures(parser);
+ } else if (parser.getName().equals("failure")) {
+ if ("urn:ietf:params:xml:ns:xmpp-sasl".equals(parser.getNamespace(null))) {
+ final Failure failure = PacketParserUtils.parseSASLFailure(parser);
+ connection.getSASLAuthentication().authenticationFailed();
+ connection.processPacket(failure);
+ }
+ } else if (parser.getName().equals("error")) {
+ throw new XMPPException(PacketParserUtils.parseStreamError(parser));
+ }
+ }
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+ }
+ catch (Exception e) {
+ if (connection.isConnected()) {
+ connection.notifyConnectionError(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Parse and setup the XML stream features.
+ *
+ * @param parser the XML parser, positioned at the start of a message packet.
+ * @throws Exception if an exception occurs while parsing the packet.
+ */
+ private void parseFeatures(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ 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(
+ PacketParserUtils.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("register")) {
+ connection.getAccountManager().setSupportsAccountCreation(
+ true);
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("features")) {
+ done = true;
+ }
+ }
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/Chat.java b/src/org/jivesoftware/smack/Chat.java
new file mode 100644
index 0000000..66f5a54
--- /dev/null
+++ b/src/org/jivesoftware/smack/Chat.java
@@ -0,0 +1,180 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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 java.util.Set;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * 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.
+ *
+ * @author Matt Tucker
+ */
+public class Chat {
+
+ private ChatManager chatManager;
+ private String threadID;
+ private String participant;
+ private final Set<MessageListener> listeners = new CopyOnWriteArraySet<MessageListener>();
+
+ /**
+ * Creates a new chat with the specified user and thread ID.
+ *
+ * @param chatManager the chatManager the chat will use.
+ * @param participant the user to chat with.
+ * @param threadID the thread ID to use.
+ */
+ Chat(ChatManager chatManager, String participant, String threadID) {
+ this.chatManager = chatManager;
+ this.participant = participant;
+ this.threadID = threadID;
+ }
+
+ /**
+ * 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 = new Message(participant, Message.Type.chat);
+ message.setThread(threadID);
+ message.setBody(text);
+ chatManager.sendMessage(this, 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.
+ *
+ * @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);
+ chatManager.sendMessage(this, message);
+ }
+
+ /**
+ * Adds a packet listener that will be notified of any new messages in the
+ * chat.
+ *
+ * @param listener a packet listener.
+ */
+ public void addMessageListener(MessageListener listener) {
+ if(listener == null) {
+ return;
+ }
+ // TODO these references should be weak.
+ listeners.add(listener);
+ }
+
+ public void removeMessageListener(MessageListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * Returns an unmodifiable collection of all of the listeners registered with this chat.
+ *
+ * @return an unmodifiable collection of all of the listeners registered with this chat.
+ */
+ public Collection<MessageListener> getListeners() {
+ return Collections.unmodifiableCollection(listeners);
+ }
+
+ /**
+ * Creates a {@link org.jivesoftware.smack.PacketCollector} which will accumulate the Messages
+ * for this chat. Always cancel PacketCollectors when finished with them as they will accumulate
+ * messages indefinitely.
+ *
+ * @return the PacketCollector which returns Messages for this chat.
+ */
+ public PacketCollector createCollector() {
+ return chatManager.createPacketCollector(this);
+ }
+
+ /**
+ * 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 Connection 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);
+
+ for (MessageListener listener : listeners) {
+ listener.processMessage(this, message);
+ }
+ }
+
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof Chat
+ && threadID.equals(((Chat)obj).getThreadID())
+ && participant.equals(((Chat)obj).getParticipant());
+ }
+} \ No newline at end of file
diff --git a/src/org/jivesoftware/smack/ChatManager.java b/src/org/jivesoftware/smack/ChatManager.java
new file mode 100644
index 0000000..22dc3f9
--- /dev/null
+++ b/src/org/jivesoftware/smack/ChatManager.java
@@ -0,0 +1,284 @@
+/**
+ * $RCSfile$
+ * $Revision: 2407 $
+ * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $
+ *
+ * Copyright 2003-2007 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;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.FromContainsFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.ThreadFilter;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smack.util.collections.ReferenceMap;
+
+import java.util.*;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * The chat manager keeps track of references to all current chats. It will not hold any references
+ * in memory on its own so it is neccesary to keep a reference to the chat object itself. To be
+ * made aware of new chats, register a listener by calling {@link #addChatListener(ChatManagerListener)}.
+ *
+ * @author Alexander Wenckus
+ */
+public class ChatManager {
+
+ /**
+ * 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++);
+ }
+
+ /**
+ * 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;
+
+ /**
+ * Maps thread ID to chat.
+ */
+ private Map<String, Chat> threadChats = Collections.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
+ ReferenceMap.WEAK));
+
+ /**
+ * Maps jids to chats
+ */
+ private Map<String, Chat> jidChats = Collections.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
+ ReferenceMap.WEAK));
+
+ /**
+ * Maps base jids to chats
+ */
+ private Map<String, Chat> baseJidChats = Collections.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
+ ReferenceMap.WEAK));
+
+ private Set<ChatManagerListener> chatManagerListeners
+ = new CopyOnWriteArraySet<ChatManagerListener>();
+
+ private Map<PacketInterceptor, PacketFilter> interceptors
+ = new WeakHashMap<PacketInterceptor, PacketFilter>();
+
+ private Connection connection;
+
+ ChatManager(Connection connection) {
+ this.connection = connection;
+
+ PacketFilter filter = new PacketFilter() {
+ public boolean accept(Packet packet) {
+ if (!(packet instanceof Message)) {
+ return false;
+ }
+ Message.Type messageType = ((Message) packet).getType();
+ return messageType != Message.Type.groupchat &&
+ messageType != Message.Type.headline;
+ }
+ };
+ // Add a listener for all message packets so that we can deliver errant
+ // messages to the best Chat instance available.
+ connection.addPacketListener(new PacketListener() {
+ public void processPacket(Packet packet) {
+ Message message = (Message) packet;
+ Chat chat;
+ if (message.getThread() == null) {
+ chat = getUserChat(message.getFrom());
+ }
+ else {
+ chat = getThreadChat(message.getThread());
+ if (chat == null) {
+ // Try to locate the chat based on the sender of the message
+ chat = getUserChat(message.getFrom());
+ }
+ }
+
+ if(chat == null) {
+ chat = createChat(message);
+ }
+ deliverMessage(chat, message);
+ }
+ }, filter);
+ }
+
+ /**
+ * Creates a new chat and returns it.
+ *
+ * @param userJID the user this chat is with.
+ * @param listener the listener which will listen for new messages from this chat.
+ * @return the created chat.
+ */
+ public Chat createChat(String userJID, MessageListener listener) {
+ String threadID;
+ do {
+ threadID = nextID();
+ } while (threadChats.get(threadID) != null);
+
+ return createChat(userJID, threadID, listener);
+ }
+
+ /**
+ * Creates a new chat using the specified thread ID, then returns it.
+ *
+ * @param userJID the jid of the user this chat is with
+ * @param thread the thread of the created chat.
+ * @param listener the listener to add to the chat
+ * @return the created chat.
+ */
+ public Chat createChat(String userJID, String thread, MessageListener listener) {
+ if(thread == null) {
+ thread = nextID();
+ }
+ Chat chat = threadChats.get(thread);
+ if(chat != null) {
+ throw new IllegalArgumentException("ThreadID is already used");
+ }
+ chat = createChat(userJID, thread, true);
+ chat.addMessageListener(listener);
+ return chat;
+ }
+
+ private Chat createChat(String userJID, String threadID, boolean createdLocally) {
+ Chat chat = new Chat(this, userJID, threadID);
+ threadChats.put(threadID, chat);
+ jidChats.put(userJID, chat);
+ baseJidChats.put(StringUtils.parseBareAddress(userJID), chat);
+
+ for(ChatManagerListener listener : chatManagerListeners) {
+ listener.chatCreated(chat, createdLocally);
+ }
+
+ return chat;
+ }
+
+ private Chat createChat(Message message) {
+ String threadID = message.getThread();
+ if(threadID == null) {
+ threadID = nextID();
+ }
+ String userJID = message.getFrom();
+
+ return createChat(userJID, threadID, false);
+ }
+
+ /**
+ * Try to get a matching chat for the given user JID. Try the full
+ * JID map first, the try to match on the base JID if no match is
+ * found.
+ *
+ * @param userJID
+ * @return
+ */
+ private Chat getUserChat(String userJID) {
+ Chat match = jidChats.get(userJID);
+
+ if (match == null) {
+ match = baseJidChats.get(StringUtils.parseBareAddress(userJID));
+ }
+ return match;
+ }
+
+ public Chat getThreadChat(String thread) {
+ return threadChats.get(thread);
+ }
+
+ /**
+ * Register a new listener with the ChatManager to recieve events related to chats.
+ *
+ * @param listener the listener.
+ */
+ public void addChatListener(ChatManagerListener listener) {
+ chatManagerListeners.add(listener);
+ }
+
+ /**
+ * Removes a listener, it will no longer be notified of new events related to chats.
+ *
+ * @param listener the listener that is being removed
+ */
+ public void removeChatListener(ChatManagerListener listener) {
+ chatManagerListeners.remove(listener);
+ }
+
+ /**
+ * Returns an unmodifiable collection of all chat listeners currently registered with this
+ * manager.
+ *
+ * @return an unmodifiable collection of all chat listeners currently registered with this
+ * manager.
+ */
+ public Collection<ChatManagerListener> getChatListeners() {
+ return Collections.unmodifiableCollection(chatManagerListeners);
+ }
+
+ private void deliverMessage(Chat chat, Message message) {
+ // Here we will run any interceptors
+ chat.deliver(message);
+ }
+
+ void sendMessage(Chat chat, Message message) {
+ for(Map.Entry<PacketInterceptor, PacketFilter> interceptor : interceptors.entrySet()) {
+ PacketFilter filter = interceptor.getValue();
+ if(filter != null && filter.accept(message)) {
+ interceptor.getKey().interceptPacket(message);
+ }
+ }
+ // Ensure that messages being sent have a proper FROM value
+ if (message.getFrom() == null) {
+ message.setFrom(connection.getUser());
+ }
+ connection.sendPacket(message);
+ }
+
+ PacketCollector createPacketCollector(Chat chat) {
+ return connection.createPacketCollector(new AndFilter(new ThreadFilter(chat.getThreadID()),
+ new FromContainsFilter(chat.getParticipant())));
+ }
+
+ /**
+ * Adds an interceptor which intercepts any messages sent through chats.
+ *
+ * @param packetInterceptor the interceptor.
+ */
+ public void addOutgoingMessageInterceptor(PacketInterceptor packetInterceptor) {
+ addOutgoingMessageInterceptor(packetInterceptor, null);
+ }
+
+ public void addOutgoingMessageInterceptor(PacketInterceptor packetInterceptor, PacketFilter filter) {
+ if (packetInterceptor != null) {
+ interceptors.put(packetInterceptor, filter);
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/ChatManagerListener.java b/src/org/jivesoftware/smack/ChatManagerListener.java
new file mode 100644
index 0000000..d7d5ab7
--- /dev/null
+++ b/src/org/jivesoftware/smack/ChatManagerListener.java
@@ -0,0 +1,37 @@
+/**
+ * $RCSfile$
+ * $Revision: 2407 $
+ * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $
+ *
+ * Copyright 2003-2007 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;
+
+/**
+ * A listener for chat related events.
+ *
+ * @author Alexander Wenckus
+ */
+public interface ChatManagerListener {
+
+ /**
+ * Event fired when a new chat is created.
+ *
+ * @param chat the chat that was created.
+ * @param createdLocally true if the chat was created by the local user and false if it wasn't.
+ */
+ void chatCreated(Chat chat, boolean createdLocally);
+}
diff --git a/src/org/jivesoftware/smack/Connection.java b/src/org/jivesoftware/smack/Connection.java
new file mode 100644
index 0000000..c6b4b1c
--- /dev/null
+++ b/src/org/jivesoftware/smack/Connection.java
@@ -0,0 +1,920 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 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.Reader;
+import java.io.Writer;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jivesoftware.smack.compression.JzlibInputOutputStream;
+import org.jivesoftware.smack.compression.XMPPInputOutputStream;
+import org.jivesoftware.smack.compression.Java7ZlibInputOutputStream;
+import org.jivesoftware.smack.debugger.SmackDebugger;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+
+/**
+ * The abstract Connection class provides an interface for connections to a
+ * XMPP server and implements shared methods which are used by the
+ * different types of connections (e.g. XMPPConnection or BoshConnection).
+ *
+ * To create a connection to a XMPP server a simple usage of this API might
+ * look like the following:
+ * <pre>
+ * // Create a connection to the igniterealtime.org XMPP server.
+ * Connection con = new XMPPConnection("igniterealtime.org");
+ * // Connect to the server
+ * con.connect();
+ * // 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 = connection.getChatManager().createChat("jdoe@igniterealtime.org"</font>, new MessageListener() {
+ * <p/>
+ * public void processMessage(Chat chat, Message message) {
+ * // Print out any messages we get back to standard out.
+ * System.out.println(<font color="green">"Received message: "</font> + message);
+ * }
+ * });
+ * chat.sendMessage(<font color="green">"Howdy!"</font>);
+ * // Disconnect from the server
+ * con.disconnect();
+ * </pre>
+ * <p/>
+ * Connections can be reused between connections. This means that an Connection
+ * may be connected, disconnected and then connected again. Listeners of the Connection
+ * will be retained accross connections.<p>
+ * <p/>
+ * If a connected Connection gets disconnected abruptly then it will try to reconnect
+ * again. To stop the reconnection process, use {@link #disconnect()}. Once stopped
+ * you can use {@link #connect()} to manually connect to the server.
+ *
+ * @see XMPPConnection
+ * @author Matt Tucker
+ * @author Guenther Niess
+ */
+public abstract class Connection {
+
+ /**
+ * Counter to uniquely identify connections that are created.
+ */
+ private final static AtomicInteger connectionCounter = new AtomicInteger(0);
+
+ /**
+ * A set of listeners which will be invoked if a new connection is created.
+ */
+ private final static Set<ConnectionCreationListener> connectionEstablishedListeners =
+ new CopyOnWriteArraySet<ConnectionCreationListener>();
+
+ protected final static List<XMPPInputOutputStream> compressionHandlers = new ArrayList<XMPPInputOutputStream>(2);
+
+ /**
+ * 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>
+ * <p/>
+ * 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;
+
+ 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();
+ // Add the Java7 compression handler first, since it's preferred
+ compressionHandlers.add(new Java7ZlibInputOutputStream());
+ // If we don't have access to the Java7 API use the JZlib compression handler
+ compressionHandlers.add(new JzlibInputOutputStream());
+ }
+
+ /**
+ * A collection of ConnectionListeners which listen for connection closing
+ * and reconnection events.
+ */
+ protected final Collection<ConnectionListener> connectionListeners =
+ new CopyOnWriteArrayList<ConnectionListener>();
+
+ /**
+ * A collection of PacketCollectors which collects packets for a specified filter
+ * and perform blocking and polling operations on the result queue.
+ */
+ protected final Collection<PacketCollector> collectors = new ConcurrentLinkedQueue<PacketCollector>();
+
+ /**
+ * List of PacketListeners that will be notified when a new packet was received.
+ */
+ protected final Map<PacketListener, ListenerWrapper> recvListeners =
+ new ConcurrentHashMap<PacketListener, ListenerWrapper>();
+
+ /**
+ * List of PacketListeners that will be notified when a new packet was sent.
+ */
+ protected final Map<PacketListener, ListenerWrapper> sendListeners =
+ new ConcurrentHashMap<PacketListener, ListenerWrapper>();
+
+ /**
+ * List of PacketInterceptors 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.
+ */
+ protected final Map<PacketInterceptor, InterceptorWrapper> interceptors =
+ new ConcurrentHashMap<PacketInterceptor, InterceptorWrapper>();
+
+ /**
+ * The AccountManager allows creation and management of accounts on an XMPP server.
+ */
+ private AccountManager accountManager = null;
+
+ /**
+ * The ChatManager keeps track of references to all current chats.
+ */
+ protected ChatManager chatManager = null;
+
+ /**
+ * The SmackDebugger allows to log and debug XML traffic.
+ */
+ protected SmackDebugger debugger = null;
+
+ /**
+ * The Reader which is used for the {@see debugger}.
+ */
+ protected Reader reader;
+
+ /**
+ * The Writer which is used for the {@see debugger}.
+ */
+ protected Writer writer;
+
+ /**
+ * The permanent storage for the roster
+ */
+ protected RosterStorage rosterStorage;
+
+
+ /**
+ * The SASLAuthentication manager that is responsible for authenticating with the server.
+ */
+ protected SASLAuthentication saslAuthentication = new SASLAuthentication(this);
+
+ /**
+ * A number to uniquely identify connections that are created. This is distinct from the
+ * connection ID, which is a value sent by the server once a connection is made.
+ */
+ protected final int connectionCounterValue = connectionCounter.getAndIncrement();
+
+ /**
+ * Holds the initial configuration used while creating the connection.
+ */
+ protected final ConnectionConfiguration config;
+
+ /**
+ * Holds the Caps Node information for the used XMPP service (i.e. the XMPP server)
+ */
+ private String serviceCapsNode;
+
+ protected XMPPInputOutputStream compressionHandler;
+
+ /**
+ * Create a new Connection to a XMPP server.
+ *
+ * @param configuration The configuration which is used to establish the connection.
+ */
+ protected Connection(ConnectionConfiguration configuration) {
+ config = configuration;
+ }
+
+ /**
+ * Returns the configuration used to connect to the server.
+ *
+ * @return the configuration used to connect to the server.
+ */
+ protected ConnectionConfiguration getConfiguration() {
+ return config;
+ }
+
+ /**
+ * Returns the name of the service provided by the XMPP server for this connection.
+ * This is also called XMPP domain of the connected server. 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 config.getServiceName();
+ }
+
+ /**
+ * 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 config.getHost();
+ }
+
+ /**
+ * 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 config.getPort();
+ }
+
+ /**
+ * 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 abstract String getUser();
+
+ /**
+ * 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. This value will be <tt>null</tt> if not connected to the server.
+ *
+ * @return the ID of this connection returned from the XMPP server or <tt>null</tt> if
+ * not connected to the server.
+ */
+ public abstract String getConnectionID();
+
+ /**
+ * Returns true if currently connected to the XMPP server.
+ *
+ * @return true if connected.
+ */
+ public abstract boolean isConnected();
+
+ /**
+ * Returns true if currently authenticated by successfully calling the login method.
+ *
+ * @return true if authenticated.
+ */
+ public abstract boolean isAuthenticated();
+
+ /**
+ * Returns true if currently authenticated anonymously.
+ *
+ * @return true if authenticated anonymously.
+ */
+ public abstract boolean isAnonymous();
+
+ /**
+ * Returns true if the connection to the server has successfully negotiated encryption.
+ *
+ * @return true if a secure connection to the server.
+ */
+ public abstract boolean isSecureConnection();
+
+ /**
+ * Returns if the reconnection mechanism is allowed to be used. By default
+ * reconnection is allowed.
+ *
+ * @return true if the reconnection mechanism is allowed to be used.
+ */
+ protected boolean isReconnectionAllowed() {
+ return config.isReconnectionAllowed();
+ }
+
+ /**
+ * 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.
+ *
+ * @return true if network traffic is being compressed.
+ */
+ public abstract boolean isUsingCompression();
+
+ /**
+ * Establishes a connection to the XMPP server and performs an automatic login
+ * only if the previous connection state was logged (authenticated). It basically
+ * creates and maintains a connection to the server.<p>
+ * <p/>
+ * Listeners will be preserved from a previous connection if the reconnection
+ * occurs after an abrupt termination.
+ *
+ * @throws XMPPException if an error occurs while trying to establish the connection.
+ */
+ public abstract void connect() throws XMPPException;
+
+ /**
+ * Logs in to the server using the strongest authentication mode supported by
+ * the server, then sets presence to available. If the server supports SASL authentication
+ * then the user will be authenticated using SASL if not Non-SASL authentication will
+ * be tried. 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.<p>
+ *
+ * Before logging in (i.e. authenticate) to the server the connection must be connected.
+ *
+ * It is possible to log in without sending an initial available presence by using
+ * {@link ConnectionConfiguration#setSendPresence(boolean)}. If this connection is
+ * not interested in loading its roster upon login then use
+ * {@link ConnectionConfiguration#setRosterLoadedAtLogin(boolean)}.
+ * Finally, if you want to not pass a password and instead use a more advanced mechanism
+ * while using SASL then you may be interested in using
+ * {@link ConnectionConfiguration#setCallbackHandler(javax.security.auth.callback.CallbackHandler)}.
+ * For more advanced login settings see {@link ConnectionConfiguration}.
+ *
+ * @param username the username.
+ * @param password the password or <tt>null</tt> if using a CallbackHandler.
+ * @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 the server supports SASL authentication
+ * then the user will be authenticated using SASL if not Non-SASL authentication will
+ * be tried. 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.<p>
+ *
+ * Before logging in (i.e. authenticate) to the server the connection must be connected.
+ *
+ * It is possible to log in without sending an initial available presence by using
+ * {@link ConnectionConfiguration#setSendPresence(boolean)}. If this connection is
+ * not interested in loading its roster upon login then use
+ * {@link ConnectionConfiguration#setRosterLoadedAtLogin(boolean)}.
+ * Finally, if you want to not pass a password and instead use a more advanced mechanism
+ * while using SASL then you may be interested in using
+ * {@link ConnectionConfiguration#setCallbackHandler(javax.security.auth.callback.CallbackHandler)}.
+ * For more advanced login settings see {@link ConnectionConfiguration}.
+ *
+ * @param username the username.
+ * @param password the password or <tt>null</tt> if using a CallbackHandler.
+ * @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 abstract void login(String username, String password, String resource) throws XMPPException;
+
+ /**
+ * 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 "123ABC@server/789XYZ" or
+ * "server/123ABC" (where "123ABC" and "789XYZ" 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 abstract void loginAnonymously() throws XMPPException;
+
+ /**
+ * Sends the specified packet to the server.
+ *
+ * @param packet the packet to send.
+ */
+ public abstract void sendPacket(Packet packet);
+
+ /**
+ * Returns an account manager instance for this connection.
+ *
+ * @return an account manager for this connection.
+ */
+ public AccountManager getAccountManager() {
+ if (accountManager == null) {
+ accountManager = new AccountManager(this);
+ }
+ return accountManager;
+ }
+
+ /**
+ * Returns a chat manager instance for this connection. The ChatManager manages all incoming and
+ * outgoing chats on the current connection.
+ *
+ * @return a chat manager instance for this connection.
+ */
+ public synchronized ChatManager getChatManager() {
+ if (this.chatManager == null) {
+ this.chatManager = new ChatManager(this);
+ }
+ return this.chatManager;
+ }
+
+ /**
+ * Returns the roster for the user.
+ * <p>
+ * This method will never return <code>null</code>, instead if the user has not yet logged into
+ * the server or is logged in anonymously all modifying methods of the returned roster object
+ * like {@link Roster#createEntry(String, String, String[])},
+ * {@link Roster#removeEntry(RosterEntry)} , etc. except adding or removing
+ * {@link RosterListener}s will throw an IllegalStateException.
+ *
+ * @return the user's roster.
+ */
+ public abstract Roster getRoster();
+
+ /**
+ * Set the store for the roster of this connection. If you set the roster storage
+ * of a connection you enable support for XEP-0237 (RosterVersioning)
+ * @param store the store used for roster versioning
+ * @throws IllegalStateException if you add a roster store when roster is initializied
+ */
+ public abstract void setRosterStorage(RosterStorage storage) throws IllegalStateException;
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Closes the connection by setting presence to unavailable then closing the connection to
+ * the XMPP server. The Connection can still be used for connecting to the server
+ * again.<p>
+ * <p/>
+ * This method cleans up all resources used by the connection. Therefore, the roster,
+ * listeners and other stateful objects cannot be re-used by simply calling connect()
+ * on this connection again. This is unlike the behavior during unexpected disconnects
+ * (and subsequent connections). In that case, all state is preserved to allow for
+ * more seamless error recovery.
+ */
+ public void disconnect() {
+ disconnect(new Presence(Presence.Type.unavailable));
+ }
+
+ /**
+ * Closes the connection. A custom unavailable presence is sent to the server, followed
+ * by closing the stream. The Connection can still be used for connecting to the server
+ * again. A custom unavilable presence is useful for communicating offline presence
+ * information such as "On vacation". Typically, just the status text of the presence
+ * packet is set with online information, but most XMPP servers will deliver the full
+ * presence packet with whatever data is set.<p>
+ * <p/>
+ * This method cleans up all resources used by the connection. Therefore, the roster,
+ * listeners and other stateful objects cannot be re-used by simply calling connect()
+ * on this connection again. This is unlike the behavior during unexpected disconnects
+ * (and subsequent connections). In that case, all state is preserved to allow for
+ * more seamless error recovery.
+ *
+ * @param unavailablePresence the presence packet to send during shutdown.
+ */
+ public abstract void disconnect(Presence unavailablePresence);
+
+ /**
+ * Adds a new listener that will be notified when new Connections are created. Note
+ * that newly created connections will not be actually connected to the server.
+ *
+ * @param connectionCreationListener a listener interested on new connections.
+ */
+ public static void addConnectionCreationListener(
+ ConnectionCreationListener connectionCreationListener) {
+ connectionEstablishedListeners.add(connectionCreationListener);
+ }
+
+ /**
+ * Removes a listener that was interested in connection creation events.
+ *
+ * @param connectionCreationListener a listener interested on new connections.
+ */
+ public static void removeConnectionCreationListener(
+ ConnectionCreationListener connectionCreationListener) {
+ connectionEstablishedListeners.remove(connectionCreationListener);
+ }
+
+ /**
+ * Get the collection of listeners that are interested in connection creation events.
+ *
+ * @return a collection of listeners interested on new connections.
+ */
+ protected static Collection<ConnectionCreationListener> getConnectionCreationListeners() {
+ return Collections.unmodifiableCollection(connectionEstablishedListeners);
+ }
+
+ /**
+ * Adds a connection listener to this connection that will be notified when
+ * the connection closes or fails. The connection needs to already be connected
+ * or otherwise an IllegalStateException will be thrown.
+ *
+ * @param connectionListener a connection listener.
+ */
+ public void addConnectionListener(ConnectionListener connectionListener) {
+ if (!isConnected()) {
+ throw new IllegalStateException("Not connected to server.");
+ }
+ if (connectionListener == null) {
+ return;
+ }
+ if (!connectionListeners.contains(connectionListener)) {
+ connectionListeners.add(connectionListener);
+ }
+ }
+
+ /**
+ * Removes a connection listener from this connection.
+ *
+ * @param connectionListener a connection listener.
+ */
+ public void removeConnectionListener(ConnectionListener connectionListener) {
+ connectionListeners.remove(connectionListener);
+ }
+
+ /**
+ * Get the collection of listeners that are interested in connection events.
+ *
+ * @return a collection of listeners interested on connection events.
+ */
+ protected Collection<ConnectionListener> getConnectionListeners() {
+ return connectionListeners;
+ }
+
+ /**
+ * Creates a new packet collector for this connection. A packet filter determines
+ * which packets will be accumulated by the collector. A PacketCollector is
+ * more suitable to use than a {@link PacketListener} when you need to wait for
+ * a specific result.
+ *
+ * @param packetFilter the packet filter to use.
+ * @return a new packet collector.
+ */
+ public PacketCollector createPacketCollector(PacketFilter packetFilter) {
+ PacketCollector collector = new PacketCollector(this, packetFilter);
+ // Add the collector to the list of active collectors.
+ collectors.add(collector);
+ return collector;
+ }
+
+ /**
+ * Remove a packet collector of this connection.
+ *
+ * @param collector a packet collectors which was created for this connection.
+ */
+ protected void removePacketCollector(PacketCollector collector) {
+ collectors.remove(collector);
+ }
+
+ /**
+ * Get the collection of all packet collectors for this connection.
+ *
+ * @return a collection of packet collectors for this connection.
+ */
+ protected Collection<PacketCollector> getPacketCollectors() {
+ return collectors;
+ }
+
+ /**
+ * Registers a packet listener with this connection. A packet filter determines
+ * which packets will be delivered to the listener. If the same packet listener
+ * is added again with a different filter, only the new filter will be used.
+ *
+ * @param packetListener the packet listener to notify of new received packets.
+ * @param packetFilter the packet filter to use.
+ */
+ public void addPacketListener(PacketListener packetListener, PacketFilter packetFilter) {
+ if (packetListener == null) {
+ throw new NullPointerException("Packet listener is null.");
+ }
+ ListenerWrapper wrapper = new ListenerWrapper(packetListener, packetFilter);
+ recvListeners.put(packetListener, wrapper);
+ }
+
+ /**
+ * Removes a packet listener for received packets from this connection.
+ *
+ * @param packetListener the packet listener to remove.
+ */
+ public void removePacketListener(PacketListener packetListener) {
+ recvListeners.remove(packetListener);
+ }
+
+ /**
+ * Get a map of all packet listeners for received packets of this connection.
+ *
+ * @return a map of all packet listeners for received packets.
+ */
+ protected Map<PacketListener, ListenerWrapper> getPacketListeners() {
+ return recvListeners;
+ }
+
+ /**
+ * 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 addPacketSendingListener(PacketListener packetListener, PacketFilter packetFilter) {
+ if (packetListener == null) {
+ throw new NullPointerException("Packet listener is null.");
+ }
+ ListenerWrapper wrapper = new ListenerWrapper(packetListener, packetFilter);
+ sendListeners.put(packetListener, wrapper);
+ }
+
+ /**
+ * Removes a packet listener for sending packets from this connection.
+ *
+ * @param packetListener the packet listener to remove.
+ */
+ public void removePacketSendingListener(PacketListener packetListener) {
+ sendListeners.remove(packetListener);
+ }
+
+ /**
+ * Get a map of all packet listeners for sending packets of this connection.
+ *
+ * @return a map of all packet listeners for sent packets.
+ */
+ protected Map<PacketListener, ListenerWrapper> getPacketSendingListeners() {
+ return sendListeners;
+ }
+
+
+ /**
+ * Process all packet listeners for sending packets.
+ *
+ * @param packet the packet to process.
+ */
+ protected void firePacketSendingListeners(Packet packet) {
+ // Notify the listeners of the new sent packet
+ for (ListenerWrapper listenerWrapper : sendListeners.values()) {
+ listenerWrapper.notifyListener(packet);
+ }
+ }
+
+ /**
+ * 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 addPacketInterceptor(PacketInterceptor packetInterceptor,
+ PacketFilter packetFilter) {
+ if (packetInterceptor == null) {
+ throw new NullPointerException("Packet interceptor is null.");
+ }
+ interceptors.put(packetInterceptor, new InterceptorWrapper(packetInterceptor, packetFilter));
+ }
+
+ /**
+ * Removes a packet interceptor.
+ *
+ * @param packetInterceptor the packet interceptor to remove.
+ */
+ public void removePacketInterceptor(PacketInterceptor packetInterceptor) {
+ interceptors.remove(packetInterceptor);
+ }
+
+ public boolean isSendPresence() {
+ return config.isSendPresence();
+ }
+
+ /**
+ * Get a map of all packet interceptors for sending packets of this connection.
+ *
+ * @return a map of all packet interceptors for sending packets.
+ */
+ protected Map<PacketInterceptor, InterceptorWrapper> getPacketInterceptors() {
+ return interceptors;
+ }
+
+ /**
+ * 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
+ */
+ protected void firePacketInterceptors(Packet packet) {
+ if (packet != null) {
+ for (InterceptorWrapper interceptorWrapper : interceptors.values()) {
+ interceptorWrapper.notifyListener(packet);
+ }
+ }
+ }
+
+ /**
+ * Initialize the {@link #debugger}. You can specify a customized {@link SmackDebugger}
+ * by setup the system property <code>smack.debuggerClass</code> to the implementation.
+ *
+ * @throws IllegalStateException if the reader or writer isn't yet initialized.
+ * @throws IllegalArgumentException if the SmackDebugger can't be loaded.
+ */
+ protected void initDebugger() {
+ if (reader == null || writer == null) {
+ throw new NullPointerException("Reader or writer isn't initialized.");
+ }
+ // If debugging is enabled, we open a window and write out all network traffic.
+ if (config.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) {
+ // Ignore.
+ }
+ Class<?> debuggerClass = null;
+ if (className != null) {
+ try {
+ debuggerClass = Class.forName(className);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ if (debuggerClass == null) {
+ try {
+ debuggerClass =
+ Class.forName("de.measite.smack.AndroidDebugger");
+ }
+ catch (Exception ex) {
+ try {
+ debuggerClass =
+ Class.forName("org.jivesoftware.smack.debugger.ConsoleDebugger");
+ }
+ catch (Exception ex2) {
+ ex2.printStackTrace();
+ }
+ }
+ }
+ // Create a new debugger instance. If an exception occurs then disable the debugging
+ // option
+ try {
+ Constructor<?> constructor = debuggerClass
+ .getConstructor(Connection.class, Writer.class, Reader.class);
+ debugger = (SmackDebugger) constructor.newInstance(this, writer, reader);
+ reader = debugger.getReader();
+ writer = debugger.getWriter();
+ }
+ catch (Exception e) {
+ throw new IllegalArgumentException("Can't initialize the configured debugger!", e);
+ }
+ }
+ else {
+ // Obtain new reader and writer from the existing debugger
+ reader = debugger.newConnectionReader(reader);
+ writer = debugger.newConnectionWriter(writer);
+ }
+ }
+
+ }
+
+ /**
+ * Set the servers Entity Caps node
+ *
+ * Connection holds this information in order to avoid a dependency to
+ * smackx where EntityCapsManager lives from smack.
+ *
+ * @param node
+ */
+ protected void setServiceCapsNode(String node) {
+ serviceCapsNode = node;
+ }
+
+ /**
+ * Retrieve the servers Entity Caps node
+ *
+ * Connection holds this information in order to avoid a dependency to
+ * smackx where EntityCapsManager lives from smack.
+ *
+ * @return
+ */
+ public String getServiceCapsNode() {
+ return serviceCapsNode;
+ }
+
+ /**
+ * A wrapper class to associate a packet filter with a listener.
+ */
+ protected static class ListenerWrapper {
+
+ private PacketListener packetListener;
+ private PacketFilter packetFilter;
+
+ /**
+ * Create a class which associates a packet filter with a listener.
+ *
+ * @param packetListener the packet listener.
+ * @param packetFilter the associated filter or null if it listen for all packets.
+ */
+ public ListenerWrapper(PacketListener packetListener, PacketFilter packetFilter) {
+ this.packetListener = packetListener;
+ this.packetFilter = packetFilter;
+ }
+
+ /**
+ * Notify and process the packet listener if the filter matches the packet.
+ *
+ * @param packet the packet which was sent or received.
+ */
+ 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.
+ */
+ protected static class InterceptorWrapper {
+
+ private PacketInterceptor packetInterceptor;
+ private PacketFilter packetFilter;
+
+ /**
+ * Create a class which associates a packet filter with an interceptor.
+ *
+ * @param packetInterceptor the interceptor.
+ * @param packetFilter the associated filter or null if it intercepts all packets.
+ */
+ 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;
+ }
+
+ /**
+ * Notify and process the packet interceptor if the filter matches the packet.
+ *
+ * @param packet the packet which will be sent.
+ */
+ public void notifyListener(Packet packet) {
+ if (packetFilter == null || packetFilter.accept(packet)) {
+ packetInterceptor.interceptPacket(packet);
+ }
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/Connection.java.orig b/src/org/jivesoftware/smack/Connection.java.orig
new file mode 100644
index 0000000..6c70a82
--- /dev/null
+++ b/src/org/jivesoftware/smack/Connection.java.orig
@@ -0,0 +1,920 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 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.Reader;
+import java.io.Writer;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jivesoftware.smack.compression.JzlibInputOutputStream;
+import org.jivesoftware.smack.compression.XMPPInputOutputStream;
+import org.jivesoftware.smack.compression.Java7ZlibInputOutputStream;
+import org.jivesoftware.smack.debugger.SmackDebugger;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+
+/**
+ * The abstract Connection class provides an interface for connections to a
+ * XMPP server and implements shared methods which are used by the
+ * different types of connections (e.g. XMPPConnection or BoshConnection).
+ *
+ * To create a connection to a XMPP server a simple usage of this API might
+ * look like the following:
+ * <pre>
+ * // Create a connection to the igniterealtime.org XMPP server.
+ * Connection con = new XMPPConnection("igniterealtime.org");
+ * // Connect to the server
+ * con.connect();
+ * // 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 = connection.getChatManager().createChat("jdoe@igniterealtime.org"</font>, new MessageListener() {
+ * <p/>
+ * public void processMessage(Chat chat, Message message) {
+ * // Print out any messages we get back to standard out.
+ * System.out.println(<font color="green">"Received message: "</font> + message);
+ * }
+ * });
+ * chat.sendMessage(<font color="green">"Howdy!"</font>);
+ * // Disconnect from the server
+ * con.disconnect();
+ * </pre>
+ * <p/>
+ * Connections can be reused between connections. This means that an Connection
+ * may be connected, disconnected and then connected again. Listeners of the Connection
+ * will be retained accross connections.<p>
+ * <p/>
+ * If a connected Connection gets disconnected abruptly then it will try to reconnect
+ * again. To stop the reconnection process, use {@link #disconnect()}. Once stopped
+ * you can use {@link #connect()} to manually connect to the server.
+ *
+ * @see XMPPConnection
+ * @author Matt Tucker
+ * @author Guenther Niess
+ */
+public abstract class Connection {
+
+ /**
+ * Counter to uniquely identify connections that are created.
+ */
+ private final static AtomicInteger connectionCounter = new AtomicInteger(0);
+
+ /**
+ * A set of listeners which will be invoked if a new connection is created.
+ */
+ private final static Set<ConnectionCreationListener> connectionEstablishedListeners =
+ new CopyOnWriteArraySet<ConnectionCreationListener>();
+
+ protected final static List<XMPPInputOutputStream> compressionHandlers = new ArrayList<XMPPInputOutputStream>(2);
+
+ /**
+ * 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>
+ * <p/>
+ * 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;
+
+ 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();
+ // Add the Java7 compression handler first, since it's preferred
+ compressionHandlers.add(new Java7ZlibInputOutputStream());
+ // If we don't have access to the Java7 API use the JZlib compression handler
+ compressionHandlers.add(new JzlibInputOutputStream());
+ }
+
+ /**
+ * A collection of ConnectionListeners which listen for connection closing
+ * and reconnection events.
+ */
+ protected final Collection<ConnectionListener> connectionListeners =
+ new CopyOnWriteArrayList<ConnectionListener>();
+
+ /**
+ * A collection of PacketCollectors which collects packets for a specified filter
+ * and perform blocking and polling operations on the result queue.
+ */
+ protected final Collection<PacketCollector> collectors = new ConcurrentLinkedQueue<PacketCollector>();
+
+ /**
+ * List of PacketListeners that will be notified when a new packet was received.
+ */
+ protected final Map<PacketListener, ListenerWrapper> recvListeners =
+ new ConcurrentHashMap<PacketListener, ListenerWrapper>();
+
+ /**
+ * List of PacketListeners that will be notified when a new packet was sent.
+ */
+ protected final Map<PacketListener, ListenerWrapper> sendListeners =
+ new ConcurrentHashMap<PacketListener, ListenerWrapper>();
+
+ /**
+ * List of PacketInterceptors 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.
+ */
+ protected final Map<PacketInterceptor, InterceptorWrapper> interceptors =
+ new ConcurrentHashMap<PacketInterceptor, InterceptorWrapper>();
+
+ /**
+ * The AccountManager allows creation and management of accounts on an XMPP server.
+ */
+ private AccountManager accountManager = null;
+
+ /**
+ * The ChatManager keeps track of references to all current chats.
+ */
+ protected ChatManager chatManager = null;
+
+ /**
+ * The SmackDebugger allows to log and debug XML traffic.
+ */
+ protected SmackDebugger debugger = null;
+
+ /**
+ * The Reader which is used for the {@see debugger}.
+ */
+ protected Reader reader;
+
+ /**
+ * The Writer which is used for the {@see debugger}.
+ */
+ protected Writer writer;
+
+ /**
+ * The permanent storage for the roster
+ */
+ protected RosterStorage rosterStorage;
+
+
+ /**
+ * The SASLAuthentication manager that is responsible for authenticating with the server.
+ */
+ protected SASLAuthentication saslAuthentication = new SASLAuthentication(this);
+
+ /**
+ * A number to uniquely identify connections that are created. This is distinct from the
+ * connection ID, which is a value sent by the server once a connection is made.
+ */
+ protected final int connectionCounterValue = connectionCounter.getAndIncrement();
+
+ /**
+ * Holds the initial configuration used while creating the connection.
+ */
+ protected final ConnectionConfiguration config;
+
+ /**
+ * Holds the Caps Node information for the used XMPP service (i.e. the XMPP server)
+ */
+ private String serviceCapsNode;
+
+ protected XMPPInputOutputStream compressionHandler;
+
+ /**
+ * Create a new Connection to a XMPP server.
+ *
+ * @param configuration The configuration which is used to establish the connection.
+ */
+ protected Connection(ConnectionConfiguration configuration) {
+ config = configuration;
+ }
+
+ /**
+ * Returns the configuration used to connect to the server.
+ *
+ * @return the configuration used to connect to the server.
+ */
+ protected ConnectionConfiguration getConfiguration() {
+ return config;
+ }
+
+ /**
+ * Returns the name of the service provided by the XMPP server for this connection.
+ * This is also called XMPP domain of the connected server. 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 config.getServiceName();
+ }
+
+ /**
+ * 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 config.getHost();
+ }
+
+ /**
+ * 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 config.getPort();
+ }
+
+ /**
+ * 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 abstract String getUser();
+
+ /**
+ * 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. This value will be <tt>null</tt> if not connected to the server.
+ *
+ * @return the ID of this connection returned from the XMPP server or <tt>null</tt> if
+ * not connected to the server.
+ */
+ public abstract String getConnectionID();
+
+ /**
+ * Returns true if currently connected to the XMPP server.
+ *
+ * @return true if connected.
+ */
+ public abstract boolean isConnected();
+
+ /**
+ * Returns true if currently authenticated by successfully calling the login method.
+ *
+ * @return true if authenticated.
+ */
+ public abstract boolean isAuthenticated();
+
+ /**
+ * Returns true if currently authenticated anonymously.
+ *
+ * @return true if authenticated anonymously.
+ */
+ public abstract boolean isAnonymous();
+
+ /**
+ * Returns true if the connection to the server has successfully negotiated encryption.
+ *
+ * @return true if a secure connection to the server.
+ */
+ public abstract boolean isSecureConnection();
+
+ /**
+ * Returns if the reconnection mechanism is allowed to be used. By default
+ * reconnection is allowed.
+ *
+ * @return true if the reconnection mechanism is allowed to be used.
+ */
+ protected boolean isReconnectionAllowed() {
+ return config.isReconnectionAllowed();
+ }
+
+ /**
+ * 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.
+ *
+ * @return true if network traffic is being compressed.
+ */
+ public abstract boolean isUsingCompression();
+
+ /**
+ * Establishes a connection to the XMPP server and performs an automatic login
+ * only if the previous connection state was logged (authenticated). It basically
+ * creates and maintains a connection to the server.<p>
+ * <p/>
+ * Listeners will be preserved from a previous connection if the reconnection
+ * occurs after an abrupt termination.
+ *
+ * @throws XMPPException if an error occurs while trying to establish the connection.
+ */
+ public abstract void connect() throws XMPPException;
+
+ /**
+ * Logs in to the server using the strongest authentication mode supported by
+ * the server, then sets presence to available. If the server supports SASL authentication
+ * then the user will be authenticated using SASL if not Non-SASL authentication will
+ * be tried. 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.<p>
+ *
+ * Before logging in (i.e. authenticate) to the server the connection must be connected.
+ *
+ * It is possible to log in without sending an initial available presence by using
+ * {@link ConnectionConfiguration#setSendPresence(boolean)}. If this connection is
+ * not interested in loading its roster upon login then use
+ * {@link ConnectionConfiguration#setRosterLoadedAtLogin(boolean)}.
+ * Finally, if you want to not pass a password and instead use a more advanced mechanism
+ * while using SASL then you may be interested in using
+ * {@link ConnectionConfiguration#setCallbackHandler(javax.security.auth.callback.CallbackHandler)}.
+ * For more advanced login settings see {@link ConnectionConfiguration}.
+ *
+ * @param username the username.
+ * @param password the password or <tt>null</tt> if using a CallbackHandler.
+ * @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 the server supports SASL authentication
+ * then the user will be authenticated using SASL if not Non-SASL authentication will
+ * be tried. 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.<p>
+ *
+ * Before logging in (i.e. authenticate) to the server the connection must be connected.
+ *
+ * It is possible to log in without sending an initial available presence by using
+ * {@link ConnectionConfiguration#setSendPresence(boolean)}. If this connection is
+ * not interested in loading its roster upon login then use
+ * {@link ConnectionConfiguration#setRosterLoadedAtLogin(boolean)}.
+ * Finally, if you want to not pass a password and instead use a more advanced mechanism
+ * while using SASL then you may be interested in using
+ * {@link ConnectionConfiguration#setCallbackHandler(javax.security.auth.callback.CallbackHandler)}.
+ * For more advanced login settings see {@link ConnectionConfiguration}.
+ *
+ * @param username the username.
+ * @param password the password or <tt>null</tt> if using a CallbackHandler.
+ * @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 abstract void login(String username, String password, String resource) throws XMPPException;
+
+ /**
+ * 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 "123ABC@server/789XYZ" or
+ * "server/123ABC" (where "123ABC" and "789XYZ" 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 abstract void loginAnonymously() throws XMPPException;
+
+ /**
+ * Sends the specified packet to the server.
+ *
+ * @param packet the packet to send.
+ */
+ public abstract void sendPacket(Packet packet);
+
+ /**
+ * Returns an account manager instance for this connection.
+ *
+ * @return an account manager for this connection.
+ */
+ public AccountManager getAccountManager() {
+ if (accountManager == null) {
+ accountManager = new AccountManager(this);
+ }
+ return accountManager;
+ }
+
+ /**
+ * Returns a chat manager instance for this connection. The ChatManager manages all incoming and
+ * outgoing chats on the current connection.
+ *
+ * @return a chat manager instance for this connection.
+ */
+ public synchronized ChatManager getChatManager() {
+ if (this.chatManager == null) {
+ this.chatManager = new ChatManager(this);
+ }
+ return this.chatManager;
+ }
+
+ /**
+ * Returns the roster for the user.
+ * <p>
+ * This method will never return <code>null</code>, instead if the user has not yet logged into
+ * the server or is logged in anonymously all modifying methods of the returned roster object
+ * like {@link Roster#createEntry(String, String, String[])},
+ * {@link Roster#removeEntry(RosterEntry)} , etc. except adding or removing
+ * {@link RosterListener}s will throw an IllegalStateException.
+ *
+ * @return the user's roster.
+ */
+ public abstract Roster getRoster();
+
+ /**
+ * Set the store for the roster of this connection. If you set the roster storage
+ * of a connection you enable support for XEP-0237 (RosterVersioning)
+ * @param store the store used for roster versioning
+ * @throws IllegalStateException if you add a roster store when roster is initializied
+ */
+ public abstract void setRosterStorage(RosterStorage storage) throws IllegalStateException;
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Closes the connection by setting presence to unavailable then closing the connection to
+ * the XMPP server. The Connection can still be used for connecting to the server
+ * again.<p>
+ * <p/>
+ * This method cleans up all resources used by the connection. Therefore, the roster,
+ * listeners and other stateful objects cannot be re-used by simply calling connect()
+ * on this connection again. This is unlike the behavior during unexpected disconnects
+ * (and subsequent connections). In that case, all state is preserved to allow for
+ * more seamless error recovery.
+ */
+ public void disconnect() {
+ disconnect(new Presence(Presence.Type.unavailable));
+ }
+
+ /**
+ * Closes the connection. A custom unavailable presence is sent to the server, followed
+ * by closing the stream. The Connection can still be used for connecting to the server
+ * again. A custom unavilable presence is useful for communicating offline presence
+ * information such as "On vacation". Typically, just the status text of the presence
+ * packet is set with online information, but most XMPP servers will deliver the full
+ * presence packet with whatever data is set.<p>
+ * <p/>
+ * This method cleans up all resources used by the connection. Therefore, the roster,
+ * listeners and other stateful objects cannot be re-used by simply calling connect()
+ * on this connection again. This is unlike the behavior during unexpected disconnects
+ * (and subsequent connections). In that case, all state is preserved to allow for
+ * more seamless error recovery.
+ *
+ * @param unavailablePresence the presence packet to send during shutdown.
+ */
+ public abstract void disconnect(Presence unavailablePresence);
+
+ /**
+ * Adds a new listener that will be notified when new Connections are created. Note
+ * that newly created connections will not be actually connected to the server.
+ *
+ * @param connectionCreationListener a listener interested on new connections.
+ */
+ public static void addConnectionCreationListener(
+ ConnectionCreationListener connectionCreationListener) {
+ connectionEstablishedListeners.add(connectionCreationListener);
+ }
+
+ /**
+ * Removes a listener that was interested in connection creation events.
+ *
+ * @param connectionCreationListener a listener interested on new connections.
+ */
+ public static void removeConnectionCreationListener(
+ ConnectionCreationListener connectionCreationListener) {
+ connectionEstablishedListeners.remove(connectionCreationListener);
+ }
+
+ /**
+ * Get the collection of listeners that are interested in connection creation events.
+ *
+ * @return a collection of listeners interested on new connections.
+ */
+ protected static Collection<ConnectionCreationListener> getConnectionCreationListeners() {
+ return Collections.unmodifiableCollection(connectionEstablishedListeners);
+ }
+
+ /**
+ * Adds a connection listener to this connection that will be notified when
+ * the connection closes or fails. The connection needs to already be connected
+ * or otherwise an IllegalStateException will be thrown.
+ *
+ * @param connectionListener a connection listener.
+ */
+ public void addConnectionListener(ConnectionListener connectionListener) {
+ if (!isConnected()) {
+ throw new IllegalStateException("Not connected to server.");
+ }
+ if (connectionListener == null) {
+ return;
+ }
+ if (!connectionListeners.contains(connectionListener)) {
+ connectionListeners.add(connectionListener);
+ }
+ }
+
+ /**
+ * Removes a connection listener from this connection.
+ *
+ * @param connectionListener a connection listener.
+ */
+ public void removeConnectionListener(ConnectionListener connectionListener) {
+ connectionListeners.remove(connectionListener);
+ }
+
+ /**
+ * Get the collection of listeners that are interested in connection events.
+ *
+ * @return a collection of listeners interested on connection events.
+ */
+ protected Collection<ConnectionListener> getConnectionListeners() {
+ return connectionListeners;
+ }
+
+ /**
+ * Creates a new packet collector for this connection. A packet filter determines
+ * which packets will be accumulated by the collector. A PacketCollector is
+ * more suitable to use than a {@link PacketListener} when you need to wait for
+ * a specific result.
+ *
+ * @param packetFilter the packet filter to use.
+ * @return a new packet collector.
+ */
+ public PacketCollector createPacketCollector(PacketFilter packetFilter) {
+ PacketCollector collector = new PacketCollector(this, packetFilter);
+ // Add the collector to the list of active collectors.
+ collectors.add(collector);
+ return collector;
+ }
+
+ /**
+ * Remove a packet collector of this connection.
+ *
+ * @param collector a packet collectors which was created for this connection.
+ */
+ protected void removePacketCollector(PacketCollector collector) {
+ collectors.remove(collector);
+ }
+
+ /**
+ * Get the collection of all packet collectors for this connection.
+ *
+ * @return a collection of packet collectors for this connection.
+ */
+ protected Collection<PacketCollector> getPacketCollectors() {
+ return collectors;
+ }
+
+ /**
+ * Registers a packet listener with this connection. A packet filter determines
+ * which packets will be delivered to the listener. If the same packet listener
+ * is added again with a different filter, only the new filter will be used.
+ *
+ * @param packetListener the packet listener to notify of new received packets.
+ * @param packetFilter the packet filter to use.
+ */
+ public void addPacketListener(PacketListener packetListener, PacketFilter packetFilter) {
+ if (packetListener == null) {
+ throw new NullPointerException("Packet listener is null.");
+ }
+ ListenerWrapper wrapper = new ListenerWrapper(packetListener, packetFilter);
+ recvListeners.put(packetListener, wrapper);
+ }
+
+ /**
+ * Removes a packet listener for received packets from this connection.
+ *
+ * @param packetListener the packet listener to remove.
+ */
+ public void removePacketListener(PacketListener packetListener) {
+ recvListeners.remove(packetListener);
+ }
+
+ /**
+ * Get a map of all packet listeners for received packets of this connection.
+ *
+ * @return a map of all packet listeners for received packets.
+ */
+ protected Map<PacketListener, ListenerWrapper> getPacketListeners() {
+ return recvListeners;
+ }
+
+ /**
+ * 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 addPacketSendingListener(PacketListener packetListener, PacketFilter packetFilter) {
+ if (packetListener == null) {
+ throw new NullPointerException("Packet listener is null.");
+ }
+ ListenerWrapper wrapper = new ListenerWrapper(packetListener, packetFilter);
+ sendListeners.put(packetListener, wrapper);
+ }
+
+ /**
+ * Removes a packet listener for sending packets from this connection.
+ *
+ * @param packetListener the packet listener to remove.
+ */
+ public void removePacketSendingListener(PacketListener packetListener) {
+ sendListeners.remove(packetListener);
+ }
+
+ /**
+ * Get a map of all packet listeners for sending packets of this connection.
+ *
+ * @return a map of all packet listeners for sent packets.
+ */
+ protected Map<PacketListener, ListenerWrapper> getPacketSendingListeners() {
+ return sendListeners;
+ }
+
+
+ /**
+ * Process all packet listeners for sending packets.
+ *
+ * @param packet the packet to process.
+ */
+ protected void firePacketSendingListeners(Packet packet) {
+ // Notify the listeners of the new sent packet
+ for (ListenerWrapper listenerWrapper : sendListeners.values()) {
+ listenerWrapper.notifyListener(packet);
+ }
+ }
+
+ /**
+ * 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 addPacketInterceptor(PacketInterceptor packetInterceptor,
+ PacketFilter packetFilter) {
+ if (packetInterceptor == null) {
+ throw new NullPointerException("Packet interceptor is null.");
+ }
+ interceptors.put(packetInterceptor, new InterceptorWrapper(packetInterceptor, packetFilter));
+ }
+
+ /**
+ * Removes a packet interceptor.
+ *
+ * @param packetInterceptor the packet interceptor to remove.
+ */
+ public void removePacketInterceptor(PacketInterceptor packetInterceptor) {
+ interceptors.remove(packetInterceptor);
+ }
+
+ public boolean isSendPresence() {
+ return config.isSendPresence();
+ }
+
+ /**
+ * Get a map of all packet interceptors for sending packets of this connection.
+ *
+ * @return a map of all packet interceptors for sending packets.
+ */
+ protected Map<PacketInterceptor, InterceptorWrapper> getPacketInterceptors() {
+ return interceptors;
+ }
+
+ /**
+ * 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
+ */
+ protected void firePacketInterceptors(Packet packet) {
+ if (packet != null) {
+ for (InterceptorWrapper interceptorWrapper : interceptors.values()) {
+ interceptorWrapper.notifyListener(packet);
+ }
+ }
+ }
+
+ /**
+ * Initialize the {@link #debugger}. You can specify a customized {@link SmackDebugger}
+ * by setup the system property <code>smack.debuggerClass</code> to the implementation.
+ *
+ * @throws IllegalStateException if the reader or writer isn't yet initialized.
+ * @throws IllegalArgumentException if the SmackDebugger can't be loaded.
+ */
+ protected void initDebugger() {
+ if (reader == null || writer == null) {
+ throw new NullPointerException("Reader or writer isn't initialized.");
+ }
+ // If debugging is enabled, we open a window and write out all network traffic.
+ if (config.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) {
+ // Ignore.
+ }
+ 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(Connection.class, Writer.class, Reader.class);
+ debugger = (SmackDebugger) constructor.newInstance(this, writer, reader);
+ reader = debugger.getReader();
+ writer = debugger.getWriter();
+ }
+ catch (Exception e) {
+ throw new IllegalArgumentException("Can't initialize the configured debugger!", e);
+ }
+ }
+ else {
+ // Obtain new reader and writer from the existing debugger
+ reader = debugger.newConnectionReader(reader);
+ writer = debugger.newConnectionWriter(writer);
+ }
+ }
+
+ }
+
+ /**
+ * Set the servers Entity Caps node
+ *
+ * Connection holds this information in order to avoid a dependency to
+ * smackx where EntityCapsManager lives from smack.
+ *
+ * @param node
+ */
+ protected void setServiceCapsNode(String node) {
+ serviceCapsNode = node;
+ }
+
+ /**
+ * Retrieve the servers Entity Caps node
+ *
+ * Connection holds this information in order to avoid a dependency to
+ * smackx where EntityCapsManager lives from smack.
+ *
+ * @return
+ */
+ public String getServiceCapsNode() {
+ return serviceCapsNode;
+ }
+
+ /**
+ * A wrapper class to associate a packet filter with a listener.
+ */
+ protected static class ListenerWrapper {
+
+ private PacketListener packetListener;
+ private PacketFilter packetFilter;
+
+ /**
+ * Create a class which associates a packet filter with a listener.
+ *
+ * @param packetListener the packet listener.
+ * @param packetFilter the associated filter or null if it listen for all packets.
+ */
+ public ListenerWrapper(PacketListener packetListener, PacketFilter packetFilter) {
+ this.packetListener = packetListener;
+ this.packetFilter = packetFilter;
+ }
+
+ /**
+ * Notify and process the packet listener if the filter matches the packet.
+ *
+ * @param packet the packet which was sent or received.
+ */
+ 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.
+ */
+ protected static class InterceptorWrapper {
+
+ private PacketInterceptor packetInterceptor;
+ private PacketFilter packetFilter;
+
+ /**
+ * Create a class which associates a packet filter with an interceptor.
+ *
+ * @param packetInterceptor the interceptor.
+ * @param packetFilter the associated filter or null if it intercepts all packets.
+ */
+ 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;
+ }
+
+ /**
+ * Notify and process the packet interceptor if the filter matches the packet.
+ *
+ * @param packet the packet which will be sent.
+ */
+ public void notifyListener(Packet packet) {
+ if (packetFilter == null || packetFilter.accept(packet)) {
+ packetInterceptor.interceptPacket(packet);
+ }
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/ConnectionConfiguration.java b/src/org/jivesoftware/smack/ConnectionConfiguration.java
new file mode 100644
index 0000000..d9108d5
--- /dev/null
+++ b/src/org/jivesoftware/smack/ConnectionConfiguration.java
@@ -0,0 +1,787 @@
+/**
+ * $RCSfile$
+ * $Revision: 3306 $
+ * $Date: 2006-01-16 14:34:56 -0300 (Mon, 16 Jan 2006) $
+ *
+ * Copyright 2003-2007 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.proxy.ProxyInfo;
+import org.jivesoftware.smack.util.DNSUtil;
+import org.jivesoftware.smack.util.dns.HostAddress;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 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 if TLS, SASL, and compression are used or not.
+ *
+ * @author Gaston Dombiak
+ */
+public class ConnectionConfiguration implements Cloneable {
+
+ /**
+ * 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.
+ */
+ private String serviceName;
+
+ private String host;
+ private int port;
+ protected List<HostAddress> hostAddresses;
+
+ private String truststorePath;
+ private String truststoreType;
+ private String truststorePassword;
+ private String keystorePath;
+ private String keystoreType;
+ private String pkcs11Library;
+ private boolean verifyChainEnabled = false;
+ private boolean verifyRootCAEnabled = false;
+ private boolean selfSignedCertificateEnabled = false;
+ private boolean expiredCertificatesCheckEnabled = false;
+ private boolean notMatchingDomainCheckEnabled = false;
+ private boolean isRosterVersioningAvailable = false;
+ private SSLContext customSSLContext;
+
+ private boolean compressionEnabled = false;
+
+ private boolean saslAuthenticationEnabled = true;
+ /**
+ * Used to get information from the user
+ */
+ private CallbackHandler callbackHandler;
+
+ private boolean debuggerEnabled = Connection.DEBUG_ENABLED;
+
+ // Flag that indicates if a reconnection should be attempted when abruptly disconnected
+ private boolean reconnectionAllowed = true;
+
+ // Holds the socket factory that is used to generate the socket in the connection
+ private SocketFactory socketFactory;
+
+ // Holds the authentication information for future reconnections
+ private String username;
+ private String password;
+ private String resource;
+ private boolean sendPresence = true;
+ private boolean rosterLoadedAtLogin = true;
+ private SecurityMode securityMode = SecurityMode.enabled;
+
+ // Holds the proxy information (such as proxyhost, proxyport, username, password etc)
+ protected ProxyInfo proxy;
+
+ /**
+ * Creates a new ConnectionConfiguration for the specified service name.
+ * A DNS SRV lookup will be performed to find out the actual host address
+ * and port to use for the connection.
+ *
+ * @param serviceName the name of the service provided by an XMPP server.
+ */
+ public ConnectionConfiguration(String serviceName) {
+ // Perform DNS lookup to get host and port to use
+ hostAddresses = DNSUtil.resolveXMPPDomain(serviceName);
+ init(serviceName, ProxyInfo.forDefaultProxy());
+ }
+
+ /**
+ *
+ */
+ protected ConnectionConfiguration() {
+ /* Does nothing */
+ }
+
+ /**
+ * Creates a new ConnectionConfiguration for the specified service name
+ * with specified proxy.
+ * A DNS SRV lookup will be performed to find out the actual host address
+ * and port to use for the connection.
+ *
+ * @param serviceName the name of the service provided by an XMPP server.
+ * @param proxy the proxy through which XMPP is to be connected
+ */
+ public ConnectionConfiguration(String serviceName,ProxyInfo proxy) {
+ // Perform DNS lookup to get host and port to use
+ hostAddresses = DNSUtil.resolveXMPPDomain(serviceName);
+ init(serviceName, proxy);
+ }
+
+ /**
+ * Creates a new ConnectionConfiguration using the specified host, port and
+ * service name. This is useful for manually overriding the DNS SRV lookup
+ * process that's used with the {@link #ConnectionConfiguration(String)}
+ * constructor. For example, say that an XMPP server is running at localhost
+ * in an internal network on port 5222 but is configured to think that it's
+ * "example.com" for testing purposes. This constructor is necessary to connect
+ * to the server in that case since a DNS SRV lookup for example.com would not
+ * point to the local testing server.
+ *
+ * @param host the host where the XMPP server is running.
+ * @param port the port where the XMPP is listening.
+ * @param serviceName the name of the service provided by an XMPP server.
+ */
+ public ConnectionConfiguration(String host, int port, String serviceName) {
+ initHostAddresses(host, port);
+ init(serviceName, ProxyInfo.forDefaultProxy());
+ }
+
+ /**
+ * Creates a new ConnectionConfiguration using the specified host, port and
+ * service name. This is useful for manually overriding the DNS SRV lookup
+ * process that's used with the {@link #ConnectionConfiguration(String)}
+ * constructor. For example, say that an XMPP server is running at localhost
+ * in an internal network on port 5222 but is configured to think that it's
+ * "example.com" for testing purposes. This constructor is necessary to connect
+ * to the server in that case since a DNS SRV lookup for example.com would not
+ * point to the local testing server.
+ *
+ * @param host the host where the XMPP server is running.
+ * @param port the port where the XMPP is listening.
+ * @param serviceName the name of the service provided by an XMPP server.
+ * @param proxy the proxy through which XMPP is to be connected
+ */
+ public ConnectionConfiguration(String host, int port, String serviceName, ProxyInfo proxy) {
+ initHostAddresses(host, port);
+ init(serviceName, proxy);
+ }
+
+ /**
+ * Creates a new ConnectionConfiguration for a connection that will connect
+ * to the desired host and port.
+ *
+ * @param host the host where the XMPP server is running.
+ * @param port the port where the XMPP is listening.
+ */
+ public ConnectionConfiguration(String host, int port) {
+ initHostAddresses(host, port);
+ init(host, ProxyInfo.forDefaultProxy());
+ }
+
+ /**
+ * Creates a new ConnectionConfiguration for a connection that will connect
+ * to the desired host and port with desired proxy.
+ *
+ * @param host the host where the XMPP server is running.
+ * @param port the port where the XMPP is listening.
+ * @param proxy the proxy through which XMPP is to be connected
+ */
+ public ConnectionConfiguration(String host, int port, ProxyInfo proxy) {
+ initHostAddresses(host, port);
+ init(host, proxy);
+ }
+
+ protected void init(String serviceName, ProxyInfo proxy) {
+ this.serviceName = serviceName;
+ this.proxy = proxy;
+
+ // 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");
+ StringBuilder buffer = new StringBuilder();
+ 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";
+ keystorePath = System.getProperty("javax.net.ssl.keyStore");
+ keystoreType = "jks";
+ pkcs11Library = "pkcs11.config";
+
+ //Setting the SocketFactory according to proxy supplied
+ socketFactory = proxy.getSocketFactory();
+ }
+
+ /**
+ * Sets the server name, also known as XMPP domain of the target server.
+ *
+ * @param serviceName the XMPP domain of the target server.
+ */
+ public void setServiceName(String serviceName) {
+ this.serviceName = serviceName;
+ }
+
+ /**
+ * 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 (and therefore
+ * may not match the {@link #getServiceName service name}.
+ *
+ * @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;
+ }
+
+ public void setUsedHostAddress(HostAddress hostAddress) {
+ this.host = hostAddress.getFQDN();
+ this.port = hostAddress.getPort();
+ }
+
+ /**
+ * Returns the TLS security mode used when making the connection. By default,
+ * the mode is {@link SecurityMode#enabled}.
+ *
+ * @return the security mode.
+ */
+ public SecurityMode getSecurityMode() {
+ return securityMode;
+ }
+
+ /**
+ * Sets the TLS security mode used when making the connection. By default,
+ * the mode is {@link SecurityMode#enabled}.
+ *
+ * @param securityMode the security mode.
+ */
+ public void setSecurityMode(SecurityMode securityMode) {
+ this.securityMode = securityMode;
+ }
+
+ /**
+ * Retuns the path to the trust store file. The trust store file contains the root
+ * certificates of several well known CAs. By default, will attempt to use the
+ * 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 trust store 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;
+ }
+
+ /**
+ * Returns the trust store type, or <tt>null</tt> if it's not set.
+ *
+ * @return the trust store type.
+ */
+ public String getTruststoreType() {
+ return truststoreType;
+ }
+
+ /**
+ * Sets the trust store type.
+ *
+ * @param truststoreType the trust store type.
+ */
+ public void setTruststoreType(String truststoreType) {
+ this.truststoreType = truststoreType;
+ }
+
+ /**
+ * Returns the password to use to access the trust store file. It is assumed that all
+ * certificates share the same password in the trust store.
+ *
+ * @return the password to use to access the truststore file.
+ */
+ public String getTruststorePassword() {
+ return truststorePassword;
+ }
+
+ /**
+ * Sets the password to use to access the trust store file. It is assumed that all
+ * certificates share the same password in the trust store.
+ *
+ * @param truststorePassword the password to use to access the truststore file.
+ */
+ public void setTruststorePassword(String truststorePassword) {
+ this.truststorePassword = truststorePassword;
+ }
+
+ /**
+ * Retuns the path to the keystore file. The key store file contains the
+ * certificates that may be used to authenticate the client to the server,
+ * in the event the server requests or requires it.
+ *
+ * @return the path to the keystore file.
+ */
+ public String getKeystorePath() {
+ return keystorePath;
+ }
+
+ /**
+ * Sets the path to the keystore file. The key store file contains the
+ * certificates that may be used to authenticate the client to the server,
+ * in the event the server requests or requires it.
+ *
+ * @param keystorePath the path to the keystore file.
+ */
+ public void setKeystorePath(String keystorePath) {
+ this.keystorePath = keystorePath;
+ }
+
+ /**
+ * Returns the keystore type, or <tt>null</tt> if it's not set.
+ *
+ * @return the keystore type.
+ */
+ public String getKeystoreType() {
+ return keystoreType;
+ }
+
+ /**
+ * Sets the keystore type.
+ *
+ * @param keystoreType the keystore type.
+ */
+ public void setKeystoreType(String keystoreType) {
+ this.keystoreType = keystoreType;
+ }
+
+
+ /**
+ * Returns the PKCS11 library file location, needed when the
+ * Keystore type is PKCS11.
+ *
+ * @return the path to the PKCS11 library file
+ */
+ public String getPKCS11Library() {
+ return pkcs11Library;
+ }
+
+ /**
+ * Sets the PKCS11 library file location, needed when the
+ * Keystore type is PKCS11
+ *
+ * @param pkcs11Library the path to the PKCS11 library file
+ */
+ public void setPKCS11Library(String pkcs11Library) {
+ this.pkcs11Library = pkcs11Library;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Gets the custom SSLContext for SSL sockets. This is null by default.
+ *
+ * @return the SSLContext previously set with setCustomSSLContext() or null.
+ */
+ public SSLContext getCustomSSLContext() {
+ return this.customSSLContext;
+ }
+
+ /**
+ * Sets a custom SSLContext for creating SSL sockets. A custom Context causes all other
+ * SSL/TLS realted settings to be ignored.
+ *
+ * @param context the custom SSLContext for new sockets; null to reset default behavior.
+ */
+ public void setCustomSSLContext(SSLContext context) {
+ this.customSSLContext = context;
+ }
+
+ /**
+ * 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 whether the client will 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 Connection#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 Connection#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;
+ }
+
+ /**
+ * Sets if the reconnection mechanism is allowed to be used. By default
+ * reconnection is allowed.
+ *
+ * @param isAllowed if the reconnection mechanism is allowed to use.
+ */
+ public void setReconnectionAllowed(boolean isAllowed) {
+ this.reconnectionAllowed = isAllowed;
+ }
+ /**
+ * Returns if the reconnection mechanism is allowed to be used. By default
+ * reconnection is allowed.
+ *
+ * @return if the reconnection mechanism is allowed to be used.
+ */
+ public boolean isReconnectionAllowed() {
+ return this.reconnectionAllowed;
+ }
+
+ /**
+ * Sets the socket factory used to create new xmppConnection sockets.
+ * This is useful when connecting through SOCKS5 proxies.
+ *
+ * @param socketFactory used to create new sockets.
+ */
+ public void setSocketFactory(SocketFactory socketFactory) {
+ this.socketFactory = socketFactory;
+ }
+
+ /**
+ * Sets if an initial available presence will be sent to the server. By default
+ * an available presence will be sent to the server indicating that this presence
+ * is not online and available to receive messages. If you want to log in without
+ * being 'noticed' then pass a <tt>false</tt> value.
+ *
+ * @param sendPresence true if an initial available presence will be sent while logging in.
+ */
+ public void setSendPresence(boolean sendPresence) {
+ this.sendPresence = sendPresence;
+ }
+
+ /**
+ * Returns true if the roster will be loaded from the server when logging in. This
+ * is the common behaviour for clients but sometimes clients may want to differ this
+ * or just never do it if not interested in rosters.
+ *
+ * @return true if the roster will be loaded from the server when logging in.
+ */
+ public boolean isRosterLoadedAtLogin() {
+ return rosterLoadedAtLogin;
+ }
+
+ /**
+ * Sets if the roster will be loaded from the server when logging in. This
+ * is the common behaviour for clients but sometimes clients may want to differ this
+ * or just never do it if not interested in rosters.
+ *
+ * @param rosterLoadedAtLogin if the roster will be loaded from the server when logging in.
+ */
+ public void setRosterLoadedAtLogin(boolean rosterLoadedAtLogin) {
+ this.rosterLoadedAtLogin = rosterLoadedAtLogin;
+ }
+
+ /**
+ * Returns a CallbackHandler to obtain information, such as the password or
+ * principal information during the SASL authentication. A CallbackHandler
+ * will be used <b>ONLY</b> if no password was specified during the login while
+ * using SASL authentication.
+ *
+ * @return a CallbackHandler to obtain information, such as the password or
+ * principal information during the SASL authentication.
+ */
+ public CallbackHandler getCallbackHandler() {
+ return callbackHandler;
+ }
+
+ /**
+ * Sets a CallbackHandler to obtain information, such as the password or
+ * principal information during the SASL authentication. A CallbackHandler
+ * will be used <b>ONLY</b> if no password was specified during the login while
+ * using SASL authentication.
+ *
+ * @param callbackHandler to obtain information, such as the password or
+ * principal information during the SASL authentication.
+ */
+ public void setCallbackHandler(CallbackHandler callbackHandler) {
+ this.callbackHandler = callbackHandler;
+ }
+
+ /**
+ * Returns the socket factory used to create new xmppConnection sockets.
+ * This is useful when connecting through SOCKS5 proxies.
+ *
+ * @return socketFactory used to create new sockets.
+ */
+ public SocketFactory getSocketFactory() {
+ return this.socketFactory;
+ }
+
+ public List<HostAddress> getHostAddresses() {
+ return Collections.unmodifiableList(hostAddresses);
+ }
+
+ /**
+ * An enumeration for TLS security modes that are available when making a connection
+ * to the XMPP server.
+ */
+ public static enum SecurityMode {
+
+ /**
+ * Securirty via TLS encryption is required in order to connect. If the server
+ * does not offer TLS or if the TLS negotiaton fails, the connection to the server
+ * will fail.
+ */
+ required,
+
+ /**
+ * Security via TLS encryption is used whenever it's available. This is the
+ * default setting.
+ */
+ enabled,
+
+ /**
+ * Security via TLS encryption is disabled and only un-encrypted connections will
+ * be used. If only TLS encryption is available from the server, the connection
+ * will fail.
+ */
+ disabled
+ }
+
+ /**
+ * Returns the username to use when trying to reconnect to the server.
+ *
+ * @return the username to use when trying to reconnect to the server.
+ */
+ String getUsername() {
+ return this.username;
+ }
+
+ /**
+ * Returns the password to use when trying to reconnect to the server.
+ *
+ * @return the password to use when trying to reconnect to the server.
+ */
+ String getPassword() {
+ return this.password;
+ }
+
+ /**
+ * Returns the resource to use when trying to reconnect to the server.
+ *
+ * @return the resource to use when trying to reconnect to the server.
+ */
+ String getResource() {
+ return resource;
+ }
+
+ boolean isRosterVersioningAvailable(){
+ return isRosterVersioningAvailable;
+ }
+
+ void setRosterVersioningAvailable(boolean enabled){
+ isRosterVersioningAvailable = enabled;
+ }
+
+ /**
+ * Returns true if an available presence should be sent when logging in while reconnecting.
+ *
+ * @return true if an available presence should be sent when logging in while reconnecting
+ */
+ boolean isSendPresence() {
+ return sendPresence;
+ }
+
+ void setLoginInfo(String username, String password, String resource) {
+ this.username = username;
+ this.password = password;
+ this.resource = resource;
+ }
+
+ private void initHostAddresses(String host, int port) {
+ hostAddresses = new ArrayList<HostAddress>(1);
+ HostAddress hostAddress;
+ try {
+ hostAddress = new HostAddress(host, port);
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ hostAddresses.add(hostAddress);
+ }
+}
diff --git a/src/org/jivesoftware/smack/ConnectionCreationListener.java b/src/org/jivesoftware/smack/ConnectionCreationListener.java
new file mode 100644
index 0000000..7cbda18
--- /dev/null
+++ b/src/org/jivesoftware/smack/ConnectionCreationListener.java
@@ -0,0 +1,41 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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;
+
+/**
+ * Implementors of this interface will be notified when a new {@link Connection}
+ * has been created. The newly created connection will not be actually connected to
+ * the server. Use {@link Connection#addConnectionCreationListener(ConnectionCreationListener)}
+ * to add new listeners.
+ *
+ * @author Gaston Dombiak
+ */
+public interface ConnectionCreationListener {
+
+ /**
+ * Notification that a new connection has been created. The new connection
+ * will not yet be connected to the server.
+ *
+ * @param connection the newly created connection.
+ */
+ public void connectionCreated(Connection connection);
+
+}
diff --git a/src/org/jivesoftware/smack/ConnectionListener.java b/src/org/jivesoftware/smack/ConnectionListener.java
new file mode 100644
index 0000000..a7ceef1
--- /dev/null
+++ b/src/org/jivesoftware/smack/ConnectionListener.java
@@ -0,0 +1,69 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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
+ * and reconnection events. Listeners are registered with Connection objects.
+ *
+ * @see Connection#addConnectionListener
+ * @see Connection#removeConnectionListener
+ *
+ * @author Matt Tucker
+ */
+public interface ConnectionListener {
+
+ /**
+ * Notification that the connection was closed normally or that the reconnection
+ * process has been aborted.
+ */
+ public void connectionClosed();
+
+ /**
+ * Notification that the connection was closed due to an exception. When
+ * abruptly disconnected it is possible for the connection to try reconnecting
+ * to the server.
+ *
+ * @param e the exception.
+ */
+ public void connectionClosedOnError(Exception e);
+
+ /**
+ * The connection will retry to reconnect in the specified number of seconds.
+ *
+ * @param seconds remaining seconds before attempting a reconnection.
+ */
+ public void reconnectingIn(int seconds);
+
+ /**
+ * The connection has reconnected successfully to the server. Connections will
+ * reconnect to the server when the previous socket connection was abruptly closed.
+ */
+ public void reconnectionSuccessful();
+
+ /**
+ * An attempt to connect to the server has failed. The connection will keep trying
+ * reconnecting to the server in a moment.
+ *
+ * @param e the exception that caused the reconnection to fail.
+ */
+ public void reconnectionFailed(Exception e);
+} \ No newline at end of file
diff --git a/src/org/jivesoftware/smack/MessageListener.java b/src/org/jivesoftware/smack/MessageListener.java
new file mode 100644
index 0000000..187c56c
--- /dev/null
+++ b/src/org/jivesoftware/smack/MessageListener.java
@@ -0,0 +1,30 @@
+/**
+ * $RCSfile$
+ * $Revision: 2407 $
+ * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $
+ *
+ * Copyright 2003-2007 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;
+
+/**
+ *
+ */
+public interface MessageListener {
+ void processMessage(Chat chat, Message message);
+}
diff --git a/src/org/jivesoftware/smack/NonSASLAuthentication.java b/src/org/jivesoftware/smack/NonSASLAuthentication.java
new file mode 100644
index 0000000..88b91ce
--- /dev/null
+++ b/src/org/jivesoftware/smack/NonSASLAuthentication.java
@@ -0,0 +1,143 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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;
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
+import org.apache.harmony.javax.security.auth.callback.Callback;
+
+/**
+ * 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 Connection connection;
+
+ public NonSASLAuthentication(Connection connection) {
+ super();
+ this.connection = connection;
+ }
+
+ public String authenticate(String username, String resource, CallbackHandler cbh) throws XMPPException {
+ //Use the callback handler to determine the password, and continue on.
+ PasswordCallback pcb = new PasswordCallback("Password: ",false);
+ try {
+ cbh.handle(new Callback[]{pcb});
+ return authenticate(username, String.valueOf(pcb.getPassword()),resource);
+ } catch (Exception e) {
+ throw new XMPPException("Unable to determine password.",e);
+ }
+ }
+
+ 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.getServiceName() + "/" + ((Authentication) response).getResource();
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/OpenTrustManager.java b/src/org/jivesoftware/smack/OpenTrustManager.java
new file mode 100644
index 0000000..61ed8c6
--- /dev/null
+++ b/src/org/jivesoftware/smack/OpenTrustManager.java
@@ -0,0 +1,49 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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/src/org/jivesoftware/smack/PacketCollector.java b/src/org/jivesoftware/smack/PacketCollector.java
new file mode 100644
index 0000000..9b4b4ae
--- /dev/null
+++ b/src/org/jivesoftware/smack/PacketCollector.java
@@ -0,0 +1,160 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * 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 a configured number of packets for processing before
+ * older packets are automatically dropped. The default number is retrieved by
+ * {@link SmackConfiguration#getPacketCollectorSize()}.
+ *
+ * @see Connection#createPacketCollector(PacketFilter)
+ * @author Matt Tucker
+ */
+public class PacketCollector {
+
+ private PacketFilter packetFilter;
+ private ArrayBlockingQueue<Packet> resultQueue;
+ private Connection connection;
+ 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 conection the connection the collector is tied to.
+ * @param packetFilter determines which packets will be returned by this collector.
+ */
+ protected PacketCollector(Connection conection, PacketFilter packetFilter) {
+ this(conection, packetFilter, SmackConfiguration.getPacketCollectorSize());
+ }
+
+ /**
+ * Creates a new packet collector. If the packet filter is <tt>null</tt>, then
+ * all packets will match this collector.
+ *
+ * @param conection the connection the collector is tied to.
+ * @param packetFilter determines which packets will be returned by this collector.
+ * @param maxSize the maximum number of packets that will be stored in the collector.
+ */
+ protected PacketCollector(Connection conection, PacketFilter packetFilter, int maxSize) {
+ this.connection = conection;
+ this.packetFilter = packetFilter;
+ this.resultQueue = new ArrayBlockingQueue<Packet>(maxSize);
+ }
+
+ /**
+ * 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;
+ connection.removePacketCollector(this);
+ }
+ }
+
+ /**
+ * 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 Packet pollResult() {
+ return resultQueue.poll();
+ }
+
+ /**
+ * Returns the next available packet. The method call will block (not return)
+ * until a packet is available.
+ *
+ * @return the next available packet.
+ */
+ public Packet nextResult() {
+ try {
+ return resultQueue.take();
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 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 Packet nextResult(long timeout) {
+ try {
+ return resultQueue.poll(timeout, TimeUnit.MILLISECONDS);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 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 void processPacket(Packet packet) {
+ if (packet == null) {
+ return;
+ }
+
+ if (packetFilter == null || packetFilter.accept(packet)) {
+ while (!resultQueue.offer(packet)) {
+ // Since we know the queue is full, this poll should never actually block.
+ resultQueue.poll();
+ }
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/PacketInterceptor.java b/src/org/jivesoftware/smack/PacketInterceptor.java
new file mode 100644
index 0000000..bd89031
--- /dev/null
+++ b/src/org/jivesoftware/smack/PacketInterceptor.java
@@ -0,0 +1,49 @@
+/**
+ * $Revision: 2408 $
+ * $Date: 2004-11-02 20:53:30 -0300 (Tue, 02 Nov 2004) $
+ *
+ * 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 Connection}
+ * 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 Connection#addPacketInterceptor(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/src/org/jivesoftware/smack/PacketListener.java b/src/org/jivesoftware/smack/PacketListener.java
new file mode 100644
index 0000000..4bc83aa
--- /dev/null
+++ b/src/org/jivesoftware/smack/PacketListener.java
@@ -0,0 +1,48 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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 Connection#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/src/org/jivesoftware/smack/PacketReader.java b/src/org/jivesoftware/smack/PacketReader.java
new file mode 100644
index 0000000..05ffc67
--- /dev/null
+++ b/src/org/jivesoftware/smack/PacketReader.java
@@ -0,0 +1,429 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.Connection.ListenerWrapper;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.sasl.SASLMechanism.Challenge;
+import org.jivesoftware.smack.sasl.SASLMechanism.Failure;
+import org.jivesoftware.smack.sasl.SASLMechanism.Success;
+import org.jivesoftware.smack.util.PacketParserUtils;
+
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.util.concurrent.*;
+
+/**
+ * Listens for XML traffic from the XMPP server and parses it into packet objects.
+ * The packet reader also invokes all packet listeners and collectors.<p>
+ *
+ * @see Connection#createPacketCollector
+ * @see Connection#addPacketListener
+ * @author Matt Tucker
+ */
+class PacketReader {
+
+ private Thread readerThread;
+ private ExecutorService listenerExecutor;
+
+ private XMPPConnection connection;
+ private XmlPullParser parser;
+ volatile boolean done;
+
+ private String connectionID = null;
+
+ protected PacketReader(final XMPPConnection connection) {
+ this.connection = connection;
+ this.init();
+ }
+
+ /**
+ * Initializes the reader in order to be used. The reader is initialized during the
+ * first connection and when reconnecting due to an abruptly disconnection.
+ */
+ protected void init() {
+ done = false;
+ connectionID = null;
+
+ readerThread = new Thread() {
+ public void run() {
+ parsePackets(this);
+ }
+ };
+ readerThread.setName("Smack Packet Reader (" + connection.connectionCounterValue + ")");
+ readerThread.setDaemon(true);
+
+ // Create an executor to deliver incoming packets to listeners. We'll use a single
+ // thread with an unbounded queue.
+ listenerExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
+
+ public Thread newThread(Runnable runnable) {
+ Thread thread = new Thread(runnable,
+ "Smack Listener Processor (" + connection.connectionCounterValue + ")");
+ thread.setDaemon(true);
+ return thread;
+ }
+ });
+
+ resetParser();
+ }
+
+ /**
+ * 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.
+ */
+ synchronized public void startup() throws XMPPException {
+ readerThread.start();
+ // Wait for stream tag before returning. We'll wait a couple of seconds before
+ // giving up and throwing an error.
+ try {
+ // 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.
+ int waitTime = SmackConfiguration.getPacketReplyTimeout();
+ wait(3 * waitTime);
+ }
+ 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) {
+ for (ConnectionListener listener : connection.getConnectionListeners()) {
+ try {
+ listener.connectionClosed();
+ }
+ catch (Exception e) {
+ // Catch and print any exception so we can recover
+ // from a faulty listener and finish the shutdown process
+ e.printStackTrace();
+ }
+ }
+ }
+ done = true;
+
+ // Shut down the listener executor.
+ listenerExecutor.shutdown();
+ }
+
+ /**
+ * Cleans up all resources used by the packet reader.
+ */
+ void cleanup() {
+ connection.recvListeners.clear();
+ connection.collectors.clear();
+ }
+
+ /**
+ * 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 = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ parser.setInput(connection.reader);
+ }
+ catch (XmlPullParserException xppe) {
+ xppe.printStackTrace();
+ }
+ }
+
+ /**
+ * Parse top-level packets in order to process them further.
+ *
+ * @param thread the thread that is being used by the reader to parse incoming packets.
+ */
+ private void parsePackets(Thread thread) {
+ 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(PacketParserUtils.parseIQ(parser, connection));
+ }
+ 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.config.setServiceName(parser.getAttributeValue(i));
+ }
+ }
+ }
+ }
+ else if (parser.getName().equals("error")) {
+ throw new XMPPException(PacketParserUtils.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
+ final Failure failure = PacketParserUtils.parseSASLFailure(parser);
+ processPacket(failure);
+ connection.getSASLAuthentication().authenticationFailed();
+ }
+ }
+ else if (parser.getName().equals("challenge")) {
+ // The server is challenging the SASL authentication made by the client
+ String challengeData = parser.nextText();
+ processPacket(new Challenge(challengeData));
+ connection.getSASLAuthentication().challengeReceived(challengeData);
+ }
+ else if (parser.getName().equals("success")) {
+ processPacket(new Success(parser.nextText()));
+ // 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")) {
+ // Disconnect the connection
+ connection.disconnect();
+ }
+ }
+ eventType = parser.next();
+ } while (!done && eventType != XmlPullParser.END_DOCUMENT && thread == readerThread);
+ }
+ catch (Exception e) {
+ // The exception can be ignored if the the connection is 'done'
+ // or if the it was caused because the socket got closed
+ if (!(done || connection.isSocketClosed())) {
+ // Close the connection and notify connection listeners of the
+ // error.
+ connection.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
+ *
+ */
+ synchronized private void releaseConnectionIDLock() {
+ notify();
+ }
+
+ /**
+ * 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;
+ }
+
+ // Loop through all collectors and notify the appropriate ones.
+ for (PacketCollector collector: connection.getPacketCollectors()) {
+ collector.processPacket(packet);
+ }
+
+ // Deliver the incoming packet to listeners.
+ listenerExecutor.submit(new ListenerNotification(packet));
+ }
+
+ private void parseFeatures(XmlPullParser parser) throws Exception {
+ boolean startTLSReceived = false;
+ boolean startTLSRequired = false;
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("starttls")) {
+ startTLSReceived = true;
+ }
+ 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(PacketParserUtils.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("ver")){
+ connection.getConfiguration().setRosterVersioningAvailable(true);
+ }
+ // Set the entity caps node for the server if one is send
+ // See http://xmpp.org/extensions/xep-0115.html#stream
+ else if (parser.getName().equals("c")) {
+ String node = parser.getAttributeValue(null, "node");
+ String ver = parser.getAttributeValue(null, "ver");
+ if (ver != null && node != null) {
+ String capsNode = node + "#" + ver;
+ // In order to avoid a dependency from smack to smackx
+ // we have to set the services caps node in the connection
+ // and not directly in the EntityCapsManager
+ connection.setServiceCapsNode(capsNode);
+ }
+ }
+ 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(PacketParserUtils.parseCompressionMethods(parser));
+ }
+ else if (parser.getName().equals("register")) {
+ connection.getAccountManager().setSupportsAccountCreation(true);
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("starttls")) {
+ // Confirm the server that we want to use TLS
+ connection.startTLSReceived(startTLSRequired);
+ }
+ else if (parser.getName().equals("required") && startTLSReceived) {
+ startTLSRequired = true;
+ }
+ else if (parser.getName().equals("features")) {
+ done = true;
+ }
+ }
+ }
+
+ // If TLS is required but the server doesn't offer it, disconnect
+ // from the server and throw an error. First check if we've already negotiated TLS
+ // and are secure, however (features get parsed a second time after TLS is established).
+ if (!connection.isSecureConnection()) {
+ if (!startTLSReceived && connection.getConfiguration().getSecurityMode() ==
+ ConnectionConfiguration.SecurityMode.required)
+ {
+ throw new XMPPException("Server does not support security (TLS), " +
+ "but security required by connection configuration.",
+ new XMPPError(XMPPError.Condition.forbidden));
+ }
+ }
+
+ // Release the lock after TLS has been negotiated or we are not insterested in TLS
+ if (!startTLSReceived || connection.getConfiguration().getSecurityMode() ==
+ ConnectionConfiguration.SecurityMode.disabled)
+ {
+ releaseConnectionIDLock();
+ }
+ }
+
+ /**
+ * A runnable to notify all listeners of a packet.
+ */
+ private class ListenerNotification implements Runnable {
+
+ private Packet packet;
+
+ public ListenerNotification(Packet packet) {
+ this.packet = packet;
+ }
+
+ public void run() {
+ for (ListenerWrapper listenerWrapper : connection.recvListeners.values()) {
+ listenerWrapper.notifyListener(packet);
+ }
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/PacketWriter.java b/src/org/jivesoftware/smack/PacketWriter.java
new file mode 100644
index 0000000..675af25
--- /dev/null
+++ b/src/org/jivesoftware/smack/PacketWriter.java
@@ -0,0 +1,240 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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 java.io.IOException;
+import java.io.Writer;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * 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.
+ *
+ * @see Connection#addPacketInterceptor
+ * @see Connection#addPacketSendingListener
+ *
+ * @author Matt Tucker
+ */
+class PacketWriter {
+
+ private Thread writerThread;
+ private Thread keepAliveThread;
+ private Writer writer;
+ private XMPPConnection connection;
+ private final BlockingQueue<Packet> queue;
+ volatile boolean done;
+
+ /**
+ * Creates a new packet writer with the specified connection.
+ *
+ * @param connection the connection.
+ */
+ protected PacketWriter(XMPPConnection connection) {
+ this.queue = new ArrayBlockingQueue<Packet>(500, true);
+ this.connection = connection;
+ init();
+ }
+
+ /**
+ * Initializes the writer in order to be used. It is called at the first connection and also
+ * is invoked if the connection is disconnected by an error.
+ */
+ protected void init() {
+ this.writer = connection.writer;
+ done = false;
+
+ writerThread = new Thread() {
+ public void run() {
+ writePackets(this);
+ }
+ };
+ writerThread.setName("Smack Packet Writer (" + connection.connectionCounterValue + ")");
+ writerThread.setDaemon(true);
+ }
+
+ /**
+ * 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.
+ connection.firePacketInterceptors(packet);
+
+ try {
+ queue.put(packet);
+ }
+ catch (InterruptedException ie) {
+ ie.printStackTrace();
+ return;
+ }
+ synchronized (queue) {
+ queue.notifyAll();
+ }
+
+ // Process packet writer listeners. Note that we're using the sending
+ // thread so it's expected that listeners are fast.
+ connection.firePacketSendingListeners(packet);
+ }
+ }
+
+ /**
+ * 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;
+ synchronized (queue) {
+ queue.notifyAll();
+ }
+ // Interrupt the keep alive thread if one was created
+ if (keepAliveThread != null)
+ keepAliveThread.interrupt();
+ }
+
+ /**
+ * Cleans up all resources used by the packet writer.
+ */
+ void cleanup() {
+ connection.interceptors.clear();
+ connection.sendListeners.clear();
+ }
+
+ /**
+ * Returns the next available packet from the queue for writing.
+ *
+ * @return the next packet for writing.
+ */
+ private Packet nextPacket() {
+ Packet packet = null;
+ // Wait until there's a packet or we're done.
+ while (!done && (packet = queue.poll()) == null) {
+ try {
+ synchronized (queue) {
+ queue.wait();
+ }
+ }
+ catch (InterruptedException ie) {
+ // Do nothing
+ }
+ }
+ return packet;
+ }
+
+ private void writePackets(Thread thisThread) {
+ try {
+ // Open the stream.
+ openStream();
+ // Write out packets from the queue.
+ while (!done && (writerThread == thisThread)) {
+ Packet packet = nextPacket();
+ if (packet != null) {
+ writer.write(packet.toXML());
+ if (queue.isEmpty()) {
+ writer.flush();
+ }
+ }
+ }
+ // Flush out the rest of the queue. If the queue is extremely large, it's possible
+ // we won't have time to entirely flush it before the socket is forced closed
+ // by the shutdown process.
+ try {
+ while (!queue.isEmpty()) {
+ Packet packet = queue.remove();
+ writer.write(packet.toXML());
+ }
+ writer.flush();
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ // Delete the queue contents (hopefully nothing is left).
+ queue.clear();
+
+ // Close the stream.
+ try {
+ writer.write("</stream:stream>");
+ writer.flush();
+ }
+ catch (Exception e) {
+ // Do nothing
+ }
+ finally {
+ try {
+ writer.close();
+ }
+ catch (Exception e) {
+ // Do nothing
+ }
+ }
+ }
+ catch (IOException ioe) {
+ // The exception can be ignored if the the connection is 'done'
+ // or if the it was caused because the socket got closed
+ if (!(done || connection.isSocketClosed())) {
+ done = true;
+ // packetReader could be set to null by an concurrent disconnect() call.
+ // Therefore Prevent NPE exceptions by checking packetReader.
+ if (connection.packetReader != null) {
+ connection.notifyConnectionError(ioe);
+ }
+ }
+ }
+ }
+
+ /**
+ * 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 {
+ StringBuilder stream = new StringBuilder();
+ stream.append("<stream:stream");
+ stream.append(" to=\"").append(connection.getServiceName()).append("\"");
+ stream.append(" xmlns=\"jabber:client\"");
+ stream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
+ stream.append(" version=\"1.0\">");
+ writer.write(stream.toString());
+ writer.flush();
+ }
+}
diff --git a/src/org/jivesoftware/smack/PrivacyList.java b/src/org/jivesoftware/smack/PrivacyList.java
new file mode 100644
index 0000000..67d731d
--- /dev/null
+++ b/src/org/jivesoftware/smack/PrivacyList.java
@@ -0,0 +1,74 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.PrivacyItem;
+
+import java.util.List;
+
+/**
+ * A privacy list represents a list of contacts that is a read only class used to represent a set of allowed or blocked communications.
+ * Basically it can:<ul>
+ *
+ * <li>Handle many {@link org.jivesoftware.smack.packet.PrivacyItem}.</li>
+ * <li>Answer if it is the default list.</li>
+ * <li>Answer if it is the active list.</li>
+ * </ul>
+ *
+ * {@link PrivacyItem Privacy Items} can handle different kind of blocking communications based on JID, group,
+ * subscription type or globally.
+ *
+ * @author Francisco Vives
+ */
+public class PrivacyList {
+
+ /** Holds if it is an active list or not **/
+ private boolean isActiveList;
+ /** Holds if it is an default list or not **/
+ private boolean isDefaultList;
+ /** Holds the list name used to print **/
+ private String listName;
+ /** Holds the list of {@see PrivacyItem} **/
+ private List<PrivacyItem> items;
+
+ protected PrivacyList(boolean isActiveList, boolean isDefaultList,
+ String listName, List<PrivacyItem> privacyItems) {
+ super();
+ this.isActiveList = isActiveList;
+ this.isDefaultList = isDefaultList;
+ this.listName = listName;
+ this.items = privacyItems;
+ }
+
+ public boolean isActiveList() {
+ return isActiveList;
+ }
+
+ public boolean isDefaultList() {
+ return isDefaultList;
+ }
+
+ public List<PrivacyItem> getItems() {
+ return items;
+ }
+
+ public String toString() {
+ return listName;
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/PrivacyListListener.java b/src/org/jivesoftware/smack/PrivacyListListener.java
new file mode 100644
index 0000000..5644ed7
--- /dev/null
+++ b/src/org/jivesoftware/smack/PrivacyListListener.java
@@ -0,0 +1,51 @@
+/**
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2006-2007 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.PrivacyItem;
+
+import java.util.List;
+
+/**
+ * Interface to implement classes to listen for server events about privacy communication.
+ * Listeners are registered with the {@link PrivacyListManager}.
+ *
+ * @see PrivacyListManager#addListener
+ *
+ * @author Francisco Vives
+ */
+public interface PrivacyListListener {
+
+ /**
+ * Set or update a privacy list with PrivacyItem.
+ *
+ * @param listName the name of the new or updated privacy list.
+ * @param listItem the PrivacyItems that rules the list.
+ */
+ public void setPrivacyList(String listName, List<PrivacyItem> listItem);
+
+ /**
+ * A privacy list has been modified by another. It gets notified.
+ *
+ * @param listName the name of the updated privacy list.
+ */
+ public void updatedPrivacyList(String listName);
+
+} \ No newline at end of file
diff --git a/src/org/jivesoftware/smack/PrivacyListManager.java b/src/org/jivesoftware/smack/PrivacyListManager.java
new file mode 100644
index 0000000..4dcc9e1
--- /dev/null
+++ b/src/org/jivesoftware/smack/PrivacyListManager.java
@@ -0,0 +1,467 @@
+/**
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2006-2007 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.*;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Privacy;
+import org.jivesoftware.smack.packet.PrivacyItem;
+
+import java.util.*;
+
+/**
+ * A PrivacyListManager is used by XMPP clients to block or allow communications from other
+ * users. Use the manager to: <ul>
+ * <li>Retrieve privacy lists.
+ * <li>Add, remove, and edit privacy lists.
+ * <li>Set, change, or decline active lists.
+ * <li>Set, change, or decline the default list (i.e., the list that is active by default).
+ * </ul>
+ * Privacy Items can handle different kind of permission communications based on JID, group,
+ * subscription type or globally (@see PrivacyItem).
+ *
+ * @author Francisco Vives
+ */
+public class PrivacyListManager {
+
+ // Keep the list of instances of this class.
+ private static Map<Connection, PrivacyListManager> instances = Collections
+ .synchronizedMap(new WeakHashMap<Connection, PrivacyListManager>());
+
+ private Connection connection;
+ private final List<PrivacyListListener> listeners = new ArrayList<PrivacyListListener>();
+ PacketFilter packetFilter = new AndFilter(new IQTypeFilter(IQ.Type.SET),
+ new PacketExtensionFilter("query", "jabber:iq:privacy"));
+
+ static {
+ // Create a new PrivacyListManager on every established connection. In the init()
+ // method of PrivacyListManager, we'll add a listener that will delete the
+ // instance when the connection is closed.
+ Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+ public void connectionCreated(Connection connection) {
+ new PrivacyListManager(connection);
+ }
+ });
+ }
+ /**
+ * Creates a new privacy manager to maintain the communication privacy. Note: no
+ * information is sent to or received from the server until you attempt to
+ * get or set the privacy communication.<p>
+ *
+ * @param connection the XMPP connection.
+ */
+ private PrivacyListManager(Connection connection) {
+ this.connection = connection;
+ this.init();
+ }
+
+ /** Answer the connection userJID that owns the privacy.
+ * @return the userJID that owns the privacy
+ */
+ private String getUser() {
+ return connection.getUser();
+ }
+
+ /**
+ * Initializes the packet listeners of the connection that will notify for any set privacy
+ * package.
+ */
+ 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) {
+ // ignore
+ }
+
+ public void reconnectionFailed(Exception e) {
+ // ignore
+ }
+
+ public void reconnectingIn(int seconds) {
+ // ignore
+ }
+
+ public void reconnectionSuccessful() {
+ // ignore
+ }
+ });
+
+ connection.addPacketListener(new PacketListener() {
+ public void processPacket(Packet packet) {
+
+ if (packet == null || packet.getError() != null) {
+ return;
+ }
+ // The packet is correct.
+ Privacy privacy = (Privacy) packet;
+
+ // Notifies the event to the listeners.
+ synchronized (listeners) {
+ for (PrivacyListListener listener : listeners) {
+ // Notifies the created or updated privacy lists
+ for (Map.Entry<String,List<PrivacyItem>> entry : privacy.getItemLists().entrySet()) {
+ String listName = entry.getKey();
+ List<PrivacyItem> items = entry.getValue();
+ if (items.isEmpty()) {
+ listener.updatedPrivacyList(listName);
+ } else {
+ listener.setPrivacyList(listName, items);
+ }
+ }
+ }
+ }
+
+ // Send a result package acknowledging the reception of a privacy package.
+
+ // Prepare the IQ packet to send
+ IQ iq = new IQ() {
+ public String getChildElementXML() {
+ return "";
+ }
+ };
+ iq.setType(IQ.Type.RESULT);
+ iq.setFrom(packet.getFrom());
+ iq.setPacketID(packet.getPacketID());
+
+ // Send create & join packet.
+ connection.sendPacket(iq);
+ }
+ }, packetFilter);
+ }
+
+ /**
+ * Returns the PrivacyListManager instance associated with a given Connection.
+ *
+ * @param connection the connection used to look for the proper PrivacyListManager.
+ * @return the PrivacyListManager associated with a given Connection.
+ */
+ public static PrivacyListManager getInstanceFor(Connection connection) {
+ return instances.get(connection);
+ }
+
+ /**
+ * Send the {@link Privacy} packet to the server in order to know some privacy content and then
+ * waits for the answer.
+ *
+ * @param requestPrivacy is the {@link Privacy} packet configured properly whose XML
+ * will be sent to the server.
+ * @return a new {@link Privacy} with the data received from the server.
+ * @exception XMPPException if the request or the answer failed, it raises an exception.
+ */
+ private Privacy getRequest(Privacy requestPrivacy) throws XMPPException {
+ // The request is a get iq type
+ requestPrivacy.setType(Privacy.Type.GET);
+ requestPrivacy.setFrom(this.getUser());
+
+ // Filter packets looking for an answer from the server.
+ PacketFilter responseFilter = new PacketIDFilter(requestPrivacy.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+
+ // Send create & join packet.
+ connection.sendPacket(requestPrivacy);
+
+ // Wait up to a certain number of seconds for a reply.
+ Privacy privacyAnswer =
+ (Privacy) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Stop queuing results
+ response.cancel();
+
+ // Interprete the result and answer the privacy only if it is valid
+ if (privacyAnswer == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (privacyAnswer.getError() != null) {
+ throw new XMPPException(privacyAnswer.getError());
+ }
+ return privacyAnswer;
+ }
+
+ /**
+ * Send the {@link Privacy} packet to the server in order to modify the server privacy and
+ * waits for the answer.
+ *
+ * @param requestPrivacy is the {@link Privacy} packet configured properly whose xml will be sent
+ * to the server.
+ * @return a new {@link Privacy} with the data received from the server.
+ * @exception XMPPException if the request or the answer failed, it raises an exception.
+ */
+ private Packet setRequest(Privacy requestPrivacy) throws XMPPException {
+
+ // The request is a get iq type
+ requestPrivacy.setType(Privacy.Type.SET);
+ requestPrivacy.setFrom(this.getUser());
+
+ // Filter packets looking for an answer from the server.
+ PacketFilter responseFilter = new PacketIDFilter(requestPrivacy.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+
+ // Send create & join packet.
+ connection.sendPacket(requestPrivacy);
+
+ // Wait up to a certain number of seconds for a reply.
+ Packet privacyAnswer = response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Stop queuing results
+ response.cancel();
+
+ // Interprete the result and answer the privacy only if it is valid
+ if (privacyAnswer == null) {
+ throw new XMPPException("No response from server.");
+ } else if (privacyAnswer.getError() != null) {
+ throw new XMPPException(privacyAnswer.getError());
+ }
+ return privacyAnswer;
+ }
+
+ /**
+ * Answer a privacy containing the list structre without {@link PrivacyItem}.
+ *
+ * @return a Privacy with the list names.
+ * @throws XMPPException if an error occurs.
+ */
+ private Privacy getPrivacyWithListNames() throws XMPPException {
+
+ // The request of the list is an empty privacy message
+ Privacy request = new Privacy();
+
+ // Send the package to the server and get the answer
+ return getRequest(request);
+ }
+
+ /**
+ * Answer the active privacy list.
+ *
+ * @return the privacy list of the active list.
+ * @throws XMPPException if an error occurs.
+ */
+ public PrivacyList getActiveList() throws XMPPException {
+ Privacy privacyAnswer = this.getPrivacyWithListNames();
+ String listName = privacyAnswer.getActiveName();
+ boolean isDefaultAndActive = privacyAnswer.getActiveName() != null
+ && privacyAnswer.getDefaultName() != null
+ && privacyAnswer.getActiveName().equals(
+ privacyAnswer.getDefaultName());
+ return new PrivacyList(true, isDefaultAndActive, listName, getPrivacyListItems(listName));
+ }
+
+ /**
+ * Answer the default privacy list.
+ *
+ * @return the privacy list of the default list.
+ * @throws XMPPException if an error occurs.
+ */
+ public PrivacyList getDefaultList() throws XMPPException {
+ Privacy privacyAnswer = this.getPrivacyWithListNames();
+ String listName = privacyAnswer.getDefaultName();
+ boolean isDefaultAndActive = privacyAnswer.getActiveName() != null
+ && privacyAnswer.getDefaultName() != null
+ && privacyAnswer.getActiveName().equals(
+ privacyAnswer.getDefaultName());
+ return new PrivacyList(isDefaultAndActive, true, listName, getPrivacyListItems(listName));
+ }
+
+ /**
+ * Answer the privacy list items under listName with the allowed and blocked permissions.
+ *
+ * @param listName the name of the list to get the allowed and blocked permissions.
+ * @return a list of privacy items under the list listName.
+ * @throws XMPPException if an error occurs.
+ */
+ private List<PrivacyItem> getPrivacyListItems(String listName) throws XMPPException {
+
+ // The request of the list is an privacy message with an empty list
+ Privacy request = new Privacy();
+ request.setPrivacyList(listName, new ArrayList<PrivacyItem>());
+
+ // Send the package to the server and get the answer
+ Privacy privacyAnswer = getRequest(request);
+
+ return privacyAnswer.getPrivacyList(listName);
+ }
+
+ /**
+ * Answer the privacy list items under listName with the allowed and blocked permissions.
+ *
+ * @param listName the name of the list to get the allowed and blocked permissions.
+ * @return a privacy list under the list listName.
+ * @throws XMPPException if an error occurs.
+ */
+ public PrivacyList getPrivacyList(String listName) throws XMPPException {
+
+ return new PrivacyList(false, false, listName, getPrivacyListItems(listName));
+ }
+
+ /**
+ * Answer every privacy list with the allowed and blocked permissions.
+ *
+ * @return an array of privacy lists.
+ * @throws XMPPException if an error occurs.
+ */
+ public PrivacyList[] getPrivacyLists() throws XMPPException {
+ Privacy privacyAnswer = this.getPrivacyWithListNames();
+ Set<String> names = privacyAnswer.getPrivacyListNames();
+ PrivacyList[] lists = new PrivacyList[names.size()];
+ boolean isActiveList;
+ boolean isDefaultList;
+ int index=0;
+ for (String listName : names) {
+ isActiveList = listName.equals(privacyAnswer.getActiveName());
+ isDefaultList = listName.equals(privacyAnswer.getDefaultName());
+ lists[index] = new PrivacyList(isActiveList, isDefaultList,
+ listName, getPrivacyListItems(listName));
+ index = index + 1;
+ }
+ return lists;
+ }
+
+
+ /**
+ * Set or change the active list to listName.
+ *
+ * @param listName the list name to set as the active one.
+ * @exception XMPPException if the request or the answer failed, it raises an exception.
+ */
+ public void setActiveListName(String listName) throws XMPPException {
+
+ // The request of the list is an privacy message with an empty list
+ Privacy request = new Privacy();
+ request.setActiveName(listName);
+
+ // Send the package to the server
+ setRequest(request);
+ }
+
+ /**
+ * Client declines the use of active lists.
+ *
+ * @throws XMPPException if an error occurs.
+ */
+ public void declineActiveList() throws XMPPException {
+
+ // The request of the list is an privacy message with an empty list
+ Privacy request = new Privacy();
+ request.setDeclineActiveList(true);
+
+ // Send the package to the server
+ setRequest(request);
+ }
+
+ /**
+ * Set or change the default list to listName.
+ *
+ * @param listName the list name to set as the default one.
+ * @exception XMPPException if the request or the answer failed, it raises an exception.
+ */
+ public void setDefaultListName(String listName) throws XMPPException {
+
+ // The request of the list is an privacy message with an empty list
+ Privacy request = new Privacy();
+ request.setDefaultName(listName);
+
+ // Send the package to the server
+ setRequest(request);
+ }
+
+ /**
+ * Client declines the use of default lists.
+ *
+ * @throws XMPPException if an error occurs.
+ */
+ public void declineDefaultList() throws XMPPException {
+
+ // The request of the list is an privacy message with an empty list
+ Privacy request = new Privacy();
+ request.setDeclineDefaultList(true);
+
+ // Send the package to the server
+ setRequest(request);
+ }
+
+ /**
+ * The client has created a new list. It send the new one to the server.
+ *
+ * @param listName the list that has changed its content.
+ * @param privacyItems a List with every privacy item in the list.
+ * @throws XMPPException if an error occurs.
+ */
+ public void createPrivacyList(String listName, List<PrivacyItem> privacyItems) throws XMPPException {
+
+ this.updatePrivacyList(listName, privacyItems);
+ }
+
+ /**
+ * The client has edited an existing list. It updates the server content with the resulting
+ * list of privacy items. The {@link PrivacyItem} list MUST contain all elements in the
+ * list (not the "delta").
+ *
+ * @param listName the list that has changed its content.
+ * @param privacyItems a List with every privacy item in the list.
+ * @throws XMPPException if an error occurs.
+ */
+ public void updatePrivacyList(String listName, List<PrivacyItem> privacyItems) throws XMPPException {
+
+ // Build the privacy package to add or update the new list
+ Privacy request = new Privacy();
+ request.setPrivacyList(listName, privacyItems);
+
+ // Send the package to the server
+ setRequest(request);
+ }
+
+ /**
+ * Remove a privacy list.
+ *
+ * @param listName the list that has changed its content.
+ * @throws XMPPException if an error occurs.
+ */
+ public void deletePrivacyList(String listName) throws XMPPException {
+
+ // The request of the list is an privacy message with an empty list
+ Privacy request = new Privacy();
+ request.setPrivacyList(listName, new ArrayList<PrivacyItem>());
+
+ // Send the package to the server
+ setRequest(request);
+ }
+
+ /**
+ * Adds a packet listener that will be notified of any new update in the user
+ * privacy communication.
+ *
+ * @param listener a packet listener.
+ */
+ public void addListener(PrivacyListListener listener) {
+ // Keep track of the listener so that we can manually deliver extra
+ // messages to it later if needed.
+ synchronized (listeners) {
+ listeners.add(listener);
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/ReconnectionManager.java b/src/org/jivesoftware/smack/ReconnectionManager.java
new file mode 100644
index 0000000..cc3e3af
--- /dev/null
+++ b/src/org/jivesoftware/smack/ReconnectionManager.java
@@ -0,0 +1,227 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.StreamError;
+import java.util.Random;
+/**
+ * Handles the automatic reconnection process. Every time a connection is dropped without
+ * the application explictly closing it, the manager automatically tries to reconnect to
+ * the server.<p>
+ *
+ * The reconnection mechanism will try to reconnect periodically:
+ * <ol>
+ * <li>For the first minute it will attempt to connect once every ten seconds.
+ * <li>For the next five minutes it will attempt to connect once a minute.
+ * <li>If that fails it will indefinitely try to connect once every five minutes.
+ * </ol>
+ *
+ * @author Francisco Vives
+ */
+public class ReconnectionManager implements ConnectionListener {
+
+ // Holds the connection to the server
+ private Connection connection;
+ private Thread reconnectionThread;
+ private int randomBase = new Random().nextInt(11) + 5; // between 5 and 15 seconds
+
+ // Holds the state of the reconnection
+ boolean done = false;
+
+ static {
+ // Create a new PrivacyListManager on every established connection. In the init()
+ // method of PrivacyListManager, we'll add a listener that will delete the
+ // instance when the connection is closed.
+ Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+ public void connectionCreated(Connection connection) {
+ connection.addConnectionListener(new ReconnectionManager(connection));
+ }
+ });
+ }
+
+ private ReconnectionManager(Connection connection) {
+ this.connection = connection;
+ }
+
+
+ /**
+ * Returns true if the reconnection mechanism is enabled.
+ *
+ * @return true if automatic reconnections are allowed.
+ */
+ private boolean isReconnectionAllowed() {
+ return !done && !connection.isConnected()
+ && connection.isReconnectionAllowed();
+ }
+
+ /**
+ * Starts a reconnection mechanism if it was configured to do that.
+ * The algorithm is been executed when the first connection error is detected.
+ * <p/>
+ * The reconnection mechanism will try to reconnect periodically in this way:
+ * <ol>
+ * <li>First it will try 6 times every 10 seconds.
+ * <li>Then it will try 10 times every 1 minute.
+ * <li>Finally it will try indefinitely every 5 minutes.
+ * </ol>
+ */
+ synchronized protected void reconnect() {
+ if (this.isReconnectionAllowed()) {
+ // Since there is no thread running, creates a new one to attempt
+ // the reconnection.
+ // avoid to run duplicated reconnectionThread -- fd: 16/09/2010
+ if (reconnectionThread!=null && reconnectionThread.isAlive()) return;
+
+ reconnectionThread = new Thread() {
+
+ /**
+ * Holds the current number of reconnection attempts
+ */
+ private int attempts = 0;
+
+ /**
+ * Returns the number of seconds until the next reconnection attempt.
+ *
+ * @return the number of seconds until the next reconnection attempt.
+ */
+ private int timeDelay() {
+ attempts++;
+ if (attempts > 13) {
+ return randomBase*6*5; // between 2.5 and 7.5 minutes (~5 minutes)
+ }
+ if (attempts > 7) {
+ return randomBase*6; // between 30 and 90 seconds (~1 minutes)
+ }
+ return randomBase; // 10 seconds
+ }
+
+ /**
+ * The process will try the reconnection until the connection succeed or the user
+ * cancell it
+ */
+ public void run() {
+ // The process will try to reconnect until the connection is established or
+ // the user cancel the reconnection process {@link Connection#disconnect()}
+ while (ReconnectionManager.this.isReconnectionAllowed()) {
+ // Find how much time we should wait until the next reconnection
+ int remainingSeconds = timeDelay();
+ // Sleep until we're ready for the next reconnection attempt. Notify
+ // listeners once per second about how much time remains before the next
+ // reconnection attempt.
+ while (ReconnectionManager.this.isReconnectionAllowed() &&
+ remainingSeconds > 0)
+ {
+ try {
+ Thread.sleep(1000);
+ remainingSeconds--;
+ ReconnectionManager.this
+ .notifyAttemptToReconnectIn(remainingSeconds);
+ }
+ catch (InterruptedException e1) {
+ e1.printStackTrace();
+ // Notify the reconnection has failed
+ ReconnectionManager.this.notifyReconnectionFailed(e1);
+ }
+ }
+
+ // Makes a reconnection attempt
+ try {
+ if (ReconnectionManager.this.isReconnectionAllowed()) {
+ connection.connect();
+ }
+ }
+ catch (XMPPException e) {
+ // Fires the failed reconnection notification
+ ReconnectionManager.this.notifyReconnectionFailed(e);
+ }
+ }
+ }
+ };
+ reconnectionThread.setName("Smack Reconnection Manager");
+ reconnectionThread.setDaemon(true);
+ reconnectionThread.start();
+ }
+ }
+
+ /**
+ * Fires listeners when a reconnection attempt has failed.
+ *
+ * @param exception the exception that occured.
+ */
+ protected void notifyReconnectionFailed(Exception exception) {
+ if (isReconnectionAllowed()) {
+ for (ConnectionListener listener : connection.connectionListeners) {
+ listener.reconnectionFailed(exception);
+ }
+ }
+ }
+
+ /**
+ * Fires listeners when The Connection will retry a reconnection. Expressed in seconds.
+ *
+ * @param seconds the number of seconds that a reconnection will be attempted in.
+ */
+ protected void notifyAttemptToReconnectIn(int seconds) {
+ if (isReconnectionAllowed()) {
+ for (ConnectionListener listener : connection.connectionListeners) {
+ listener.reconnectingIn(seconds);
+ }
+ }
+ }
+
+ public void connectionClosed() {
+ done = true;
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ done = false;
+ if (e instanceof XMPPException) {
+ XMPPException xmppEx = (XMPPException) e;
+ StreamError error = xmppEx.getStreamError();
+
+ // Make sure the error is not null
+ if (error != null) {
+ String reason = error.getCode();
+
+ if ("conflict".equals(reason)) {
+ return;
+ }
+ }
+ }
+
+ if (this.isReconnectionAllowed()) {
+ this.reconnect();
+ }
+ }
+
+ public void reconnectingIn(int seconds) {
+ // ignore
+ }
+
+ public void reconnectionFailed(Exception e) {
+ // ignore
+ }
+
+ /**
+ * The connection has successfull gotten connected.
+ */
+ public void reconnectionSuccessful() {
+ // ignore
+ }
+
+} \ No newline at end of file
diff --git a/src/org/jivesoftware/smack/Roster.java b/src/org/jivesoftware/smack/Roster.java
new file mode 100644
index 0000000..66a78b2
--- /dev/null
+++ b/src/org/jivesoftware/smack/Roster.java
@@ -0,0 +1,1038 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.filter.PacketIDFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.RosterPacket;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * 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>
+ * <p/>
+ * Others users may attempt to subscribe to this user using a subscription request. Three
+ * modes are supported for handling these requests: <ul>
+ * <li>{@link SubscriptionMode#accept_all accept_all} -- accept all subscription requests.</li>
+ * <li>{@link SubscriptionMode#reject_all reject_all} -- reject all subscription requests.</li>
+ * <li>{@link SubscriptionMode#manual manual} -- manually process all subscription requests.</li>
+ * </ul>
+ *
+ * @author Matt Tucker
+ * @see Connection#getRoster()
+ */
+public class Roster {
+
+ /**
+ * The default subscription processing mode to use when a Roster is created. By default
+ * all subscription requests are automatically accepted.
+ */
+ private static SubscriptionMode defaultSubscriptionMode = SubscriptionMode.accept_all;
+ private RosterStorage persistentStorage;
+
+ private Connection connection;
+ private final Map<String, RosterGroup> groups;
+ private final Map<String,RosterEntry> entries;
+ private final List<RosterEntry> unfiledEntries;
+ private final List<RosterListener> rosterListeners;
+ private Map<String, Map<String, Presence>> presenceMap;
+ // The roster is marked as initialized when at least a single roster packet
+ // has been received and processed.
+ boolean rosterInitialized = false;
+ private PresencePacketListener presencePacketListener;
+
+ private SubscriptionMode subscriptionMode = getDefaultSubscriptionMode();
+
+ private String requestPacketId;
+
+ /**
+ * 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 SubscriptionMode#accept_all}.
+ *
+ * @return the default subscription mode to use for new Rosters
+ */
+ public static SubscriptionMode 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 SubscriptionMode#accept_all}.
+ *
+ * @param subscriptionMode the default subscription mode to use for new Rosters.
+ */
+ public static void setDefaultSubscriptionMode(SubscriptionMode subscriptionMode) {
+ defaultSubscriptionMode = subscriptionMode;
+ }
+
+ Roster(final Connection connection, RosterStorage persistentStorage){
+ this(connection);
+ this.persistentStorage = persistentStorage;
+ }
+
+ /**
+ * Creates a new roster.
+ *
+ * @param connection an XMPP connection.
+ */
+ Roster(final Connection connection) {
+ this.connection = connection;
+ //Disable roster versioning if server doesn't offer support for it
+ if(!connection.getConfiguration().isRosterVersioningAvailable()){
+ persistentStorage=null;
+ }
+ groups = new ConcurrentHashMap<String, RosterGroup>();
+ unfiledEntries = new CopyOnWriteArrayList<RosterEntry>();
+ entries = new ConcurrentHashMap<String,RosterEntry>();
+ rosterListeners = new CopyOnWriteArrayList<RosterListener>();
+ presenceMap = new ConcurrentHashMap<String, Map<String, Presence>>();
+ // 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);
+ presencePacketListener = new PresencePacketListener();
+ connection.addPacketListener(presencePacketListener, presenceFilter);
+
+ // Listen for connection events
+ final ConnectionListener connectionListener = new AbstractConnectionListener() {
+
+ public void connectionClosed() {
+ // Changes the presence available contacts to unavailable
+ setOfflinePresences();
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ // Changes the presence available contacts to unavailable
+ setOfflinePresences();
+ }
+
+ };
+
+ // if not connected add listener after successful login
+ if(!this.connection.isConnected()) {
+ Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+
+ public void connectionCreated(Connection connection) {
+ if(connection.equals(Roster.this.connection)) {
+ Roster.this.connection.addConnectionListener(connectionListener);
+ }
+
+ }
+ });
+ } else {
+ connection.addConnectionListener(connectionListener);
+ }
+ }
+
+ /**
+ * 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 SubscriptionMode#accept_all}.<p>
+ * <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 SubscriptionMode 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 SubscriptionMode#accept_all}.<p>
+ * <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(SubscriptionMode subscriptionMode) {
+ 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.
+ *
+ * @throws IllegalStateException if connection is not logged in or logged in anonymously
+ */
+ public void reload() {
+ if (!connection.isAuthenticated()) {
+ throw new IllegalStateException("Not logged in to server.");
+ }
+ if (connection.isAnonymous()) {
+ throw new IllegalStateException("Anonymous users can't have a roster.");
+ }
+
+ RosterPacket packet = new RosterPacket();
+ if(persistentStorage!=null){
+ packet.setVersion(persistentStorage.getRosterVersion());
+ }
+ requestPacketId = packet.getPacketID();
+ PacketFilter idFilter = new PacketIDFilter(requestPacketId);
+ connection.addPacketListener(new RosterResultListener(), idFilter);
+ connection.sendPacket(packet);
+ }
+
+ /**
+ * 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) {
+ 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) {
+ rosterListeners.remove(rosterListener);
+ }
+
+ /**
+ * Creates a new group.<p>
+ * <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.
+ * @throws IllegalStateException if connection is not logged in or logged in anonymously
+ */
+ public RosterGroup createGroup(String name) {
+ if (!connection.isAuthenticated()) {
+ throw new IllegalStateException("Not logged in to server.");
+ }
+ if (connection.isAnonymous()) {
+ throw new IllegalStateException("Anonymous users can't have a roster.");
+ }
+ 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.
+ * @throws XMPPException if an XMPP exception occurs.
+ * @throws IllegalStateException if connection is not logged in or logged in anonymously
+ */
+ public void createEntry(String user, String name, String[] groups) throws XMPPException {
+ if (!connection.isAuthenticated()) {
+ throw new IllegalStateException("Not logged in to server.");
+ }
+ if (connection.isAnonymous()) {
+ throw new IllegalStateException("Anonymous users can't have a roster.");
+ }
+
+ // 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 (String group : groups) {
+ if (group != null && group.trim().length() > 0) {
+ item.addGroupName(group);
+ }
+ }
+ }
+ 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);
+ }
+
+ private void insertRosterItems(List<RosterPacket.Item> items){
+ Collection<String> addedEntries = new ArrayList<String>();
+ Collection<String> updatedEntries = new ArrayList<String>();
+ Collection<String> deletedEntries = new ArrayList<String>();
+ Iterator<RosterPacket.Item> iter = items.iterator();
+ while(iter.hasNext()){
+ insertRosterItem(iter.next(), addedEntries,updatedEntries,deletedEntries);
+ }
+ fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
+ }
+
+ private void insertRosterItem(RosterPacket.Item item, Collection<String> addedEntries,
+ Collection<String> updatedEntries, Collection<String> deletedEntries){
+ RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
+ item.getItemType(), item.getItemStatus(), this, 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.containsKey(item.getUser())) {
+ entries.remove(item.getUser());
+ }
+ // Remove the entry from the unfiled entry list.
+ 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
+ if(deletedEntries!=null){
+ deletedEntries.add(item.getUser());
+ }
+ }
+ else {
+ // Make sure the entry is in the entry list.
+ if (!entries.containsKey(item.getUser())) {
+ entries.put(item.getUser(), entry);
+ // Keep note that an entry has been added
+ if(addedEntries!=null){
+ addedEntries.add(item.getUser());
+ }
+ }
+ else {
+ // If the entry was in then list then update its state with the new values
+ entries.put(item.getUser(), entry);
+
+ // Keep note that an entry has been updated
+ if(updatedEntries!=null){
+ updatedEntries.add(item.getUser());
+ }
+ }
+ // If the roster entry belongs to any groups, remove it from the
+ // list of unfiled entries.
+ if (!item.getGroupNames().isEmpty()) {
+ unfiledEntries.remove(entry);
+ }
+ // Otherwise add it to the list of unfiled entries.
+ else {
+ if (!unfiledEntries.contains(entry)) {
+ unfiledEntries.add(entry);
+ }
+ }
+ }
+
+ // Find the list of groups that the user currently belongs to.
+ List<String> currentGroupNames = new ArrayList<String>();
+ for (RosterGroup group: getGroups()) {
+ if (group.contains(entry)) {
+ 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<String> newGroupNames = new ArrayList<String>();
+ for (String groupName : item.getGroupNames()) {
+ // 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 (String newGroupName : newGroupNames) {
+ currentGroupNames.remove(newGroupName);
+ }
+ }
+
+ // Loop through any groups that remain and remove the entries.
+ // This is neccessary for the case of remote entry removals.
+ for (String groupName : currentGroupNames) {
+ RosterGroup group = getGroup(groupName);
+ group.removeEntryLocal(entry);
+ if (group.getEntryCount() == 0) {
+ 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 (RosterGroup group : getGroups()) {
+ if (group.getEntryCount() == 0) {
+ groups.remove(group.getName());
+ }
+ }
+ }
+
+ /**
+ * 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.
+ * @throws XMPPException if an XMPP error occurs.
+ * @throws IllegalStateException if connection is not logged in or logged in anonymously
+ */
+ public void removeEntry(RosterEntry entry) throws XMPPException {
+ if (!connection.isAuthenticated()) {
+ throw new IllegalStateException("Not logged in to server.");
+ }
+ if (connection.isAnonymous()) {
+ throw new IllegalStateException("Anonymous users can't have a roster.");
+ }
+
+ // Only remove the entry if it's in the entry list.
+ // The actual removal logic takes place in RosterPacketListenerprocess>>Packet(Packet)
+ if (!entries.containsKey(entry.getUser())) {
+ 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());
+ }
+ }
+
+ /**
+ * Returns a count of the entries in the roster.
+ *
+ * @return the number of entries in the roster.
+ */
+ public int getEntryCount() {
+ return getEntries().size();
+ }
+
+ /**
+ * Returns an unmodifiable collection of all entries in the roster, including entries
+ * that don't belong to any groups.
+ *
+ * @return all entries in the roster.
+ */
+ public Collection<RosterEntry> getEntries() {
+ Set<RosterEntry> allEntries = new HashSet<RosterEntry>();
+ // Loop through all roster groups and add their entries to the answer
+ for (RosterGroup rosterGroup : getGroups()) {
+ allEntries.addAll(rosterGroup.getEntries());
+ }
+ // Add the roster unfiled entries to the answer
+ allEntries.addAll(unfiledEntries);
+
+ return Collections.unmodifiableCollection(allEntries);
+ }
+
+ /**
+ * 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() {
+ return unfiledEntries.size();
+ }
+
+ /**
+ * Returns an unmodifiable collection for the unfiled roster entries. An unfiled entry is
+ * an entry that doesn't belong to any groups.
+ *
+ * @return the unfiled roster entries.
+ */
+ public Collection<RosterEntry> getUnfiledEntries() {
+ return Collections.unmodifiableList(unfiledEntries);
+ }
+
+ /**
+ * 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;
+ }
+ return entries.get(user.toLowerCase());
+ }
+
+ /**
+ * 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) {
+ return groups.get(name);
+ }
+
+ /**
+ * Returns the number of the groups in the roster.
+ *
+ * @return the number of groups in the roster.
+ */
+ public int getGroupCount() {
+ return groups.size();
+ }
+
+ /**
+ * Returns an unmodifiable collections of all the roster groups.
+ *
+ * @return an iterator for all roster groups.
+ */
+ public Collection<RosterGroup> getGroups() {
+ return Collections.unmodifiableCollection(groups.values());
+ }
+
+ /**
+ * Returns the presence info for a particular user. If the user is offline, or
+ * if no presence data is available (such as when you are not subscribed to the
+ * user's presence updates), unavailable presence will be returned.<p>
+ * <p/>
+ * If the user has several presences (one for each resource), then the presence with
+ * highest priority will be returned. If multiple presences have the same priority,
+ * the one with the "most available" presence mode will be returned. In order,
+ * that's {@link org.jivesoftware.smack.packet.Presence.Mode#chat free to chat},
+ * {@link org.jivesoftware.smack.packet.Presence.Mode#available available},
+ * {@link org.jivesoftware.smack.packet.Presence.Mode#away away},
+ * {@link org.jivesoftware.smack.packet.Presence.Mode#xa extended away}, and
+ * {@link org.jivesoftware.smack.packet.Presence.Mode#dnd do not disturb}.<p>
+ * <p/>
+ * Note that presence information is received asynchronously. So, just after logging
+ * in to the server, presence values for users in the roster may be unavailable
+ * even if they are actually online. In other words, the value returned by this
+ * method should only be treated as a snapshot in time, and may not accurately reflect
+ * other user's presence instant by instant. If you need to track presence over time,
+ * such as when showing a visual representation of the roster, consider using a
+ * {@link RosterListener}.
+ *
+ * @param user an XMPP ID. The address could be in any valid format (e.g.
+ * "domain/resource", "user@domain" or "user@domain/resource"). Any resource
+ * information that's part of the ID will be discarded.
+ * @return the user's current presence, or unavailable presence if the user is offline
+ * or if no presence information is available..
+ */
+ public Presence getPresence(String user) {
+ String key = getPresenceMapKey(StringUtils.parseBareAddress(user));
+ Map<String, Presence> userPresences = presenceMap.get(key);
+ if (userPresences == null) {
+ Presence presence = new Presence(Presence.Type.unavailable);
+ presence.setFrom(user);
+ return presence;
+ }
+ else {
+ // Find the resource with the highest priority
+ // Might be changed to use the resource with the highest availability instead.
+ Presence presence = null;
+
+ for (String resource : userPresences.keySet()) {
+ Presence p = userPresences.get(resource);
+ if (!p.isAvailable()) {
+ continue;
+ }
+ // Chose presence with highest priority first.
+ if (presence == null || p.getPriority() > presence.getPriority()) {
+ presence = p;
+ }
+ // If equal priority, choose "most available" by the mode value.
+ else if (p.getPriority() == presence.getPriority()) {
+ Presence.Mode pMode = p.getMode();
+ // Default to presence mode of available.
+ if (pMode == null) {
+ pMode = Presence.Mode.available;
+ }
+ Presence.Mode presenceMode = presence.getMode();
+ // Default to presence mode of available.
+ if (presenceMode == null) {
+ presenceMode = Presence.Mode.available;
+ }
+ if (pMode.compareTo(presenceMode) < 0) {
+ presence = p;
+ }
+ }
+ }
+ if (presence == null) {
+ presence = new Presence(Presence.Type.unavailable);
+ presence.setFrom(user);
+ return presence;
+ }
+ else {
+ return presence;
+ }
+ }
+ }
+
+ /**
+ * Returns the presence info for a particular user's resource, or unavailable presence
+ * if the user is offline or if no presence information is available, such as
+ * when you are not subscribed to the user's presence updates.
+ *
+ * @param userWithResource a fully qualified XMPP ID including a resource (user@domain/resource).
+ * @return the user's current presence, or unavailable presence if the user is offline
+ * or if no presence information is available.
+ */
+ public Presence getPresenceResource(String userWithResource) {
+ String key = getPresenceMapKey(userWithResource);
+ String resource = StringUtils.parseResource(userWithResource);
+ Map<String, Presence> userPresences = presenceMap.get(key);
+ if (userPresences == null) {
+ Presence presence = new Presence(Presence.Type.unavailable);
+ presence.setFrom(userWithResource);
+ return presence;
+ }
+ else {
+ Presence presence = userPresences.get(resource);
+ if (presence == null) {
+ presence = new Presence(Presence.Type.unavailable);
+ presence.setFrom(userWithResource);
+ return presence;
+ }
+ else {
+ return presence;
+ }
+ }
+ }
+
+ /**
+ * Returns an iterator (of Presence objects) for all of a user's current presences
+ * or an unavailable presence 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 XMPP ID, e.g. jdoe@example.com.
+ * @return an iterator (of Presence objects) for all the user's current presences,
+ * or an unavailable presence if the user is offline or if no presence information
+ * is available.
+ */
+ public Iterator<Presence> getPresences(String user) {
+ String key = getPresenceMapKey(user);
+ Map<String, Presence> userPresences = presenceMap.get(key);
+ if (userPresences == null) {
+ Presence presence = new Presence(Presence.Type.unavailable);
+ presence.setFrom(user);
+ return Arrays.asList(presence).iterator();
+ }
+ else {
+ Collection<Presence> answer = new ArrayList<Presence>();
+ for (Presence presence : userPresences.values()) {
+ if (presence.isAvailable()) {
+ answer.add(presence);
+ }
+ }
+ if (!answer.isEmpty()) {
+ return answer.iterator();
+ }
+ else {
+ Presence presence = new Presence(Presence.Type.unavailable);
+ presence.setFrom(user);
+ return Arrays.asList(presence).iterator();
+ }
+ }
+ }
+
+ /**
+ * Cleans up all resources used by the roster.
+ */
+ void cleanup() {
+ rosterListeners.clear();
+ }
+
+ /**
+ * 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 bare or fully qualified XMPP ID, e.g. jdoe@example.com or
+ * 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();
+ }
+
+ /**
+ * Changes the presence of available contacts offline by simulating an unavailable
+ * presence sent from the server. After a disconnection, every Presence is set
+ * to offline.
+ */
+ private void setOfflinePresences() {
+ Presence packetUnavailable;
+ for (String user : presenceMap.keySet()) {
+ Map<String, Presence> resources = presenceMap.get(user);
+ if (resources != null) {
+ for (String resource : resources.keySet()) {
+ packetUnavailable = new Presence(Presence.Type.unavailable);
+ packetUnavailable.setFrom(user + "/" + resource);
+ presencePacketListener.processPacket(packetUnavailable);
+ }
+ }
+ }
+ }
+
+ /**
+ * 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<String> addedEntries, Collection<String> updatedEntries,
+ Collection<String> deletedEntries) {
+ for (RosterListener listener : rosterListeners) {
+ if (!addedEntries.isEmpty()) {
+ listener.entriesAdded(addedEntries);
+ }
+ if (!updatedEntries.isEmpty()) {
+ listener.entriesUpdated(updatedEntries);
+ }
+ if (!deletedEntries.isEmpty()) {
+ listener.entriesDeleted(deletedEntries);
+ }
+ }
+ }
+
+ /**
+ * Fires roster presence changed event to roster listeners.
+ *
+ * @param presence the presence change.
+ */
+ private void fireRosterPresenceEvent(Presence presence) {
+ for (RosterListener listener : rosterListeners) {
+ listener.presenceChanged(presence);
+ }
+ }
+
+ /**
+ * An enumeration for the subscription mode options.
+ */
+ public enum SubscriptionMode {
+
+ /**
+ * 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.
+ */
+ accept_all,
+
+ /**
+ * Automatically reject all subscription requests.
+ */
+ reject_all,
+
+ /**
+ * 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.
+ */
+ manual
+ }
+
+ /**
+ * 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" presence, 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<String, Presence> userPresences;
+ // Get the user presence map
+ if (presenceMap.get(key) == null) {
+ userPresences = new ConcurrentHashMap<String, Presence>();
+ presenceMap.put(key, userPresences);
+ }
+ else {
+ userPresences = presenceMap.get(key);
+ }
+ // See if an offline presence was being stored in the map. If so, remove
+ // it since we now have an online presence.
+ userPresences.remove("");
+ // Add the new presence, using the resources as a key.
+ userPresences.put(StringUtils.parseResource(from), presence);
+ // If the user is in the roster, fire an event.
+ RosterEntry entry = entries.get(key);
+ if (entry != null) {
+ fireRosterPresenceEvent(presence);
+ }
+ }
+ // If an "unavailable" packet.
+ else if (presence.getType() == Presence.Type.unavailable) {
+ // If no resource, this is likely an offline presence as part of
+ // a roster presence flood. In that case, we store it.
+ if ("".equals(StringUtils.parseResource(from))) {
+ Map<String, Presence> userPresences;
+ // Get the user presence map
+ if (presenceMap.get(key) == null) {
+ userPresences = new ConcurrentHashMap<String, Presence>();
+ presenceMap.put(key, userPresences);
+ }
+ else {
+ userPresences = presenceMap.get(key);
+ }
+ userPresences.put("", presence);
+ }
+ // Otherwise, this is a normal offline presence.
+ else if (presenceMap.get(key) != null) {
+ Map<String, Presence> userPresences = presenceMap.get(key);
+ // Store the offline presence, as it may include extra information
+ // such as the user being on vacation.
+ userPresences.put(StringUtils.parseResource(from), presence);
+ }
+ // If the user is in the roster, fire an event.
+ RosterEntry entry = entries.get(key);
+ if (entry != null) {
+ fireRosterPresenceEvent(presence);
+ }
+ }
+ else if (presence.getType() == Presence.Type.subscribe) {
+ if (subscriptionMode == SubscriptionMode.accept_all) {
+ // Accept all subscription requests.
+ Presence response = new Presence(Presence.Type.subscribed);
+ response.setTo(presence.getFrom());
+ connection.sendPacket(response);
+ }
+ else if (subscriptionMode == SubscriptionMode.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 != SubscriptionMode.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.
+ }
+ // Error presence packets from a bare JID mean we invalidate all existing
+ // presence info for the user.
+ else if (presence.getType() == Presence.Type.error &&
+ "".equals(StringUtils.parseResource(from)))
+ {
+ Map<String, Presence> userPresences;
+ if (!presenceMap.containsKey(key)) {
+ userPresences = new ConcurrentHashMap<String, Presence>();
+ presenceMap.put(key, userPresences);
+ }
+ else {
+ userPresences = presenceMap.get(key);
+ // Any other presence data is invalidated by the error packet.
+ userPresences.clear();
+ }
+ // Set the new presence using the empty resource as a key.
+ userPresences.put("", presence);
+ // If the user is in the roster, fire an event.
+ RosterEntry entry = entries.get(key);
+ if (entry != null) {
+ fireRosterPresenceEvent(presence);
+ }
+ }
+ }
+ }
+
+ /**
+ * Listen for empty IQ results which indicate that the client has already a current
+ * roster version
+ * @author Till Klocke
+ *
+ */
+
+ private class RosterResultListener implements PacketListener{
+
+ public void processPacket(Packet packet) {
+ if(packet instanceof IQ){
+ IQ result = (IQ)packet;
+ if(result.getType().equals(IQ.Type.RESULT) && result.getExtensions().isEmpty()){
+ Collection<String> addedEntries = new ArrayList<String>();
+ Collection<String> updatedEntries = new ArrayList<String>();
+ Collection<String> deletedEntries = new ArrayList<String>();
+ if(persistentStorage!=null){
+ for(RosterPacket.Item item : persistentStorage.getEntries()){
+ insertRosterItem(item,addedEntries,updatedEntries,deletedEntries);
+ }
+ }
+ synchronized (Roster.this) {
+ rosterInitialized = true;
+ Roster.this.notifyAll();
+ }
+ fireRosterChangedEvent(addedEntries,updatedEntries,deletedEntries);
+ }
+ }
+ connection.removePacketListener(this);
+ }
+ }
+
+ /**
+ * 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<String> addedEntries = new ArrayList<String>();
+ Collection<String> updatedEntries = new ArrayList<String>();
+ Collection<String> deletedEntries = new ArrayList<String>();
+
+ String version=null;
+ RosterPacket rosterPacket = (RosterPacket) packet;
+ List<RosterPacket.Item> rosterItems = new ArrayList<RosterPacket.Item>();
+ for(RosterPacket.Item item : rosterPacket.getRosterItems()){
+ rosterItems.add(item);
+ }
+ //Here we check if the server send a versioned roster, if not we do not use
+ //the roster storage to store entries and work like in the old times
+ if(rosterPacket.getVersion()==null){
+ persistentStorage=null;
+ } else{
+ version = rosterPacket.getVersion();
+ }
+
+ if(persistentStorage!=null && !rosterInitialized){
+ for(RosterPacket.Item item : persistentStorage.getEntries()){
+ rosterItems.add(item);
+ }
+ }
+
+ for (RosterPacket.Item item : rosterItems) {
+ insertRosterItem(item,addedEntries,updatedEntries,deletedEntries);
+ }
+ if(persistentStorage!=null){
+ for (RosterPacket.Item i : rosterPacket.getRosterItems()){
+ if(i.getItemType().equals(RosterPacket.ItemType.remove)){
+ persistentStorage.removeEntry(i.getUser());
+ }
+ else{
+ persistentStorage.addEntry(i, version);
+ }
+ }
+ }
+ // Mark the roster as initialized.
+ synchronized (Roster.this) {
+ rosterInitialized = true;
+ Roster.this.notifyAll();
+ }
+
+ // Fire event for roster listeners.
+ fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/RosterEntry.java b/src/org/jivesoftware/smack/RosterEntry.java
new file mode 100644
index 0000000..55b394e
--- /dev/null
+++ b/src/org/jivesoftware/smack/RosterEntry.java
@@ -0,0 +1,244 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.IQ;
+import org.jivesoftware.smack.packet.RosterPacket;
+
+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;
+ final private Roster roster;
+ final private Connection 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, Roster roster, Connection connection) {
+ this.user = user;
+ this.name = name;
+ this.type = type;
+ this.status = status;
+ this.roster = roster;
+ 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 unmodifiable collection of the roster groups that this entry belongs to.
+ *
+ * @return an iterator for the groups this entry belongs to.
+ */
+ public Collection<RosterGroup> getGroups() {
+ List<RosterGroup> results = new ArrayList<RosterGroup>();
+ // Loop through all roster groups and find the ones that contain this
+ // entry. This algorithm should be fine
+ for (RosterGroup group: roster.getGroups()) {
+ if (group.contains(this)) {
+ results.add(group);
+ }
+ }
+ return Collections.unmodifiableCollection(results);
+ }
+
+ /**
+ * Returns the roster subscription type of the entry. When the type is
+ * RosterPacket.ItemType.none or 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() {
+ StringBuilder buf = new StringBuilder();
+ if (name != null) {
+ buf.append(name).append(": ");
+ }
+ buf.append(user);
+ Collection<RosterGroup> groups = getGroups();
+ if (!groups.isEmpty()) {
+ buf.append(" [");
+ Iterator<RosterGroup> iter = groups.iterator();
+ RosterGroup group = iter.next();
+ buf.append(group.getName());
+ while (iter.hasNext()) {
+ buf.append(", ");
+ group = iter.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;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return this.user.hashCode();
+ }
+
+ /**
+ * Indicates whether some other object is "equal to" this by comparing all members.
+ * <p>
+ * The {@link #equals(Object)} method returns <code>true</code> if the user JIDs are equal.
+ *
+ * @param obj the reference object with which to compare.
+ * @return <code>true</code> if this object is the same as the obj argument; <code>false</code>
+ * otherwise.
+ */
+ public boolean equalsDeep(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ RosterEntry other = (RosterEntry) obj;
+ if (name == null) {
+ if (other.name != null)
+ return false;
+ }
+ else if (!name.equals(other.name))
+ return false;
+ if (status == null) {
+ if (other.status != null)
+ return false;
+ }
+ else if (!status.equals(other.status))
+ return false;
+ if (type == null) {
+ if (other.type != null)
+ return false;
+ }
+ else if (!type.equals(other.type))
+ return false;
+ if (user == null) {
+ if (other.user != null)
+ return false;
+ }
+ else if (!user.equals(other.user))
+ return false;
+ return true;
+ }
+
+ 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 (RosterGroup group : entry.getGroups()) {
+ item.addGroupName(group.getName());
+ }
+ return item;
+ }
+
+} \ No newline at end of file
diff --git a/src/org/jivesoftware/smack/RosterGroup.java b/src/org/jivesoftware/smack/RosterGroup.java
new file mode 100644
index 0000000..e768f6d
--- /dev/null
+++ b/src/org/jivesoftware/smack/RosterGroup.java
@@ -0,0 +1,253 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.IQ;
+import org.jivesoftware.smack.packet.RosterPacket;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A group of roster entries.
+ *
+ * @see Roster#getGroup(String)
+ * @author Matt Tucker
+ */
+public class RosterGroup {
+
+ private String name;
+ private Connection connection;
+ private final List<RosterEntry> 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, Connection connection) {
+ this.name = name;
+ this.connection = connection;
+ entries = new ArrayList<RosterEntry>();
+ }
+
+ /**
+ * 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 (RosterEntry entry : entries) {
+ RosterPacket packet = new RosterPacket();
+ packet.setType(IQ.Type.SET);
+ 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 unmodifiable collection of all entries in the group.
+ *
+ * @return all entries in the group.
+ */
+ public Collection<RosterEntry> getEntries() {
+ synchronized (entries) {
+ return Collections.unmodifiableList(new ArrayList<RosterEntry>(entries));
+ }
+ }
+
+ /**
+ * 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 (RosterEntry entry : entries) {
+ 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.
+ * Note that this is an asynchronous call -- Smack must wait for the server
+ * to receive the updated roster.
+ *
+ * @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());
+ }
+ }
+ }
+
+ /**
+ * 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.
+ * Note that this is an asynchronous call -- Smack must wait for the server
+ * to receive the updated roster.
+ *
+ * @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());
+ }
+ }
+ }
+
+ public 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/src/org/jivesoftware/smack/RosterListener.java b/src/org/jivesoftware/smack/RosterListener.java
new file mode 100644
index 0000000..8be9ddc
--- /dev/null
+++ b/src/org/jivesoftware/smack/RosterListener.java
@@ -0,0 +1,83 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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 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.
+ *
+ * @see Roster#addRosterListener(RosterListener)
+ * @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<String> 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<String> 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<String> addresses);
+
+ /**
+ * Called when the presence of a roster entry is changed. Care should be taken
+ * when using the presence data delivered as part of this event. Specifically,
+ * when a user account is online with multiple resources, the UI should account
+ * for that. For example, say a user is online with their desktop computer and
+ * mobile phone. If the user logs out of the IM client on their mobile phone, the
+ * user should not be shown in the roster (contact list) as offline since they're
+ * still available as another resource.<p>
+ *
+ * To get the current "best presence" for a user after the presence update, query the roster:
+ * <pre>
+ * String user = presence.getFrom();
+ * Presence bestPresence = roster.getPresence(user);
+ * </pre>
+ *
+ * That will return the presence value for the user with the highest priority and
+ * availability.
+ *
+ * Note that this listener is triggered for presence (mode) changes only
+ * (e.g presence of types available and unavailable. Subscription-related
+ * presence packets will not cause this method to be called.
+ *
+ * @param presence the presence that changed.
+ * @see Roster#getPresence(String)
+ */
+ public void presenceChanged(Presence presence);
+} \ No newline at end of file
diff --git a/src/org/jivesoftware/smack/RosterStorage.java b/src/org/jivesoftware/smack/RosterStorage.java
new file mode 100644
index 0000000..8c5f386
--- /dev/null
+++ b/src/org/jivesoftware/smack/RosterStorage.java
@@ -0,0 +1,54 @@
+package org.jivesoftware.smack;
+
+import java.util.List;
+
+import org.jivesoftware.smack.packet.RosterPacket;
+
+/**
+ * This is an interface for persistent roster storage needed to implement XEP-0237
+ * @author Till Klocke
+ *
+ */
+
+public interface RosterStorage {
+
+ /**
+ * This method returns a List object with all RosterEntries contained in this store.
+ * @return List object with all entries in local roster storage
+ */
+ public List<RosterPacket.Item> getEntries();
+ /**
+ * This method returns the RosterEntry which belongs to a specific user.
+ * @param bareJid The bare JID of the RosterEntry
+ * @return The RosterEntry which belongs to that user
+ */
+ public RosterPacket.Item getEntry(String bareJid);
+ /**
+ * Returns the number of entries in this roster store
+ * @return the number of entries
+ */
+ public int getEntryCount();
+ /**
+ * This methos returns the version number as specified by the "ver" attribute
+ * of the local store. Should return an emtpy string if store is empty.
+ * @return local roster version
+ */
+ public String getRosterVersion();
+ /**
+ * This method stores a new RosterEntry in this store or overrides an existing one.
+ * If ver is null an IllegalArgumentException should be thrown.
+ * @param entry the entry to save
+ * @param ver the version this roster push contained
+ */
+ public void addEntry(RosterPacket.Item item, String ver);
+ /**
+ * Removes an entry from the persistent storage
+ * @param bareJid The bare JID of the entry to be removed
+ */
+ public void removeEntry(String bareJid);
+ /**
+ * Update an entry which has been modified locally
+ * @param entry the entry to be updated
+ */
+ public void updateLocalEntry(RosterPacket.Item item);
+}
diff --git a/src/org/jivesoftware/smack/SASLAuthentication.java b/src/org/jivesoftware/smack/SASLAuthentication.java
new file mode 100644
index 0000000..d7a7449
--- /dev/null
+++ b/src/org/jivesoftware/smack/SASLAuthentication.java
@@ -0,0 +1,586 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.Packet;
+import org.jivesoftware.smack.packet.Session;
+import org.jivesoftware.smack.sasl.*;
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.util.*;
+
+/**
+ * <p>This class is responsible authenticating the user using SASL, binding the resource
+ * to the connection and establishing a session with the server.</p>
+ *
+ * <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>
+ *
+ * <p>The server may support many SASL mechanisms to use for authenticating. Out of the box
+ * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use
+ * {@link #registerSASLMechanism(String, Class)} to register a new mechanisms. A registered
+ * mechanism wont be used until {@link #supportSASLMechanism(String, int)} is called. By default,
+ * the list of supported SASL mechanisms is determined from the {@link SmackConfiguration}. </p>
+ *
+ * <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>
+ *
+ * <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.</p>
+ *
+ * @see org.jivesoftware.smack.sasl.SASLMechanism
+ *
+ * @author Gaston Dombiak
+ * @author Jay Kline
+ */
+public class SASLAuthentication implements UserAuthentication {
+
+ private static Map<String, Class<? extends SASLMechanism>> implementedMechanisms = new HashMap<String, Class<? extends SASLMechanism>>();
+ private static List<String> mechanismsPreferences = new ArrayList<String>();
+
+ private Connection connection;
+ private Collection<String> serverMechanisms = new ArrayList<String>();
+ private SASLMechanism currentMechanism = null;
+ /**
+ * Boolean indicating if SASL negotiation has finished and was successful.
+ */
+ private boolean saslNegotiated;
+ /**
+ * Boolean indication if SASL authentication has failed. When failed the server may end
+ * the connection.
+ */
+ private boolean saslFailed;
+ private boolean resourceBinded;
+ private boolean sessionSupported;
+ /**
+ * The SASL related error condition if there was one provided by the server.
+ */
+ private String errorCondition;
+
+ static {
+
+ // Register SASL mechanisms supported by Smack
+ registerSASLMechanism("EXTERNAL", SASLExternalMechanism.class);
+ registerSASLMechanism("GSSAPI", SASLGSSAPIMechanism.class);
+ registerSASLMechanism("DIGEST-MD5", SASLDigestMD5Mechanism.class);
+ registerSASLMechanism("CRAM-MD5", SASLCramMD5Mechanism.class);
+ registerSASLMechanism("PLAIN", SASLPlainMechanism.class);
+ registerSASLMechanism("ANONYMOUS", SASLAnonymous.class);
+
+// supportSASLMechanism("GSSAPI",0);
+ supportSASLMechanism("DIGEST-MD5",0);
+// supportSASLMechanism("CRAM-MD5",2);
+ supportSASLMechanism("PLAIN",1);
+ supportSASLMechanism("ANONYMOUS",2);
+
+ }
+
+ /**
+ * Registers a new SASL mechanism
+ *
+ * @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(String name, Class<? extends SASLMechanism> mClass) {
+ implementedMechanisms.put(name, mClass);
+ }
+
+ /**
+ * Unregisters an existing SASL mechanism. Once the mechanism has been unregistered it won't
+ * be possible to authenticate users using the removed SASL mechanism. It also removes the
+ * mechanism from the supported list.
+ *
+ * @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);
+ }
+
+
+ /**
+ * 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.
+ * The SASL mechanism must be registered via {@link #registerSASLMechanism(String, Class)}
+ *
+ * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
+ */
+ public static void supportSASLMechanism(String name) {
+ mechanismsPreferences.add(0, name);
+ }
+
+ /**
+ * 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.
+ * 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. The SASL mechanism must be
+ * registered via {@link #registerSASLMechanism(String, Class)}
+ *
+ * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
+ * @param index preference position amongst all the implemented SASL mechanism. Starts with 0.
+ */
+ public static void supportSASLMechanism(String name, int index) {
+ mechanismsPreferences.add(index, name);
+ }
+
+ /**
+ * Un-supports an existing SASL mechanism. Once the mechanism has been unregistered it won't
+ * be possible to authenticate users using the removed SASL mechanism. Note that the mechanism
+ * is still registered, but will just not be used.
+ *
+ * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
+ */
+ public static void unsupportSASLMechanism(String 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<Class<? extends SASLMechanism>> getRegisterSASLMechanisms() {
+ List<Class<? extends SASLMechanism>> answer = new ArrayList<Class<? extends SASLMechanism>>();
+ for (String mechanismsPreference : mechanismsPreferences) {
+ answer.add(implementedMechanisms.get(mechanismsPreference));
+ }
+ return answer;
+ }
+
+ SASLAuthentication(Connection connection) {
+ super();
+ this.connection = connection;
+ this.init();
+ }
+
+ /**
+ * 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() {
+ return !serverMechanisms.isEmpty() && (serverMechanisms.size() != 1 || !hasAnonymousAuthentication());
+ }
+
+ /**
+ * 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 resource the desired resource.
+ * @param cbh the CallbackHandler used to get information from the user
+ * @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 resource, CallbackHandler cbh)
+ throws XMPPException {
+ // Locate the SASLMechanism to use
+ String selectedMechanism = null;
+ for (String mechanism : mechanismsPreferences) {
+ if (implementedMechanisms.containsKey(mechanism) &&
+ serverMechanisms.contains(mechanism)) {
+ selectedMechanism = mechanism;
+ break;
+ }
+ }
+ if (selectedMechanism != null) {
+ // A SASL mechanism was found. Authenticate using the selected mechanism and then
+ // proceed to bind a resource
+ try {
+ Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
+ Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);
+ currentMechanism = constructor.newInstance(this);
+ // Trigger SASL authentication with the selected mechanism. We use
+ // connection.getHost() since GSAPI requires the FQDN of the server, which
+ // may not match the XMPP domain.
+ currentMechanism.authenticate(username, connection.getHost(), cbh);
+
+ // Wait until SASL negotiation finishes
+ synchronized (this) {
+ if (!saslNegotiated && !saslFailed) {
+ try {
+ wait(30000);
+ }
+ catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+ }
+
+ if (saslFailed) {
+ // SASL authentication failed and the server may have closed the connection
+ // so throw an exception
+ if (errorCondition != null) {
+ throw new XMPPException("SASL authentication " +
+ selectedMechanism + " failed: " + errorCondition);
+ }
+ else {
+ throw new XMPPException("SASL authentication failed using mechanism " +
+ selectedMechanism);
+ }
+ }
+
+ if (saslNegotiated) {
+ // Bind a resource for this connection and
+ return bindResourceAndEstablishSession(resource);
+ } else {
+ // SASL authentication failed
+ }
+ }
+ catch (XMPPException e) {
+ throw e;
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ else {
+ throw new XMPPException("SASL Authentication failed. No known authentication mechanisims.");
+ }
+ throw new XMPPException("SASL authentication failed");
+ }
+
+ /**
+ * 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
+ String selectedMechanism = null;
+ for (String mechanism : mechanismsPreferences) {
+ if (implementedMechanisms.containsKey(mechanism) &&
+ serverMechanisms.contains(mechanism)) {
+ selectedMechanism = mechanism;
+ break;
+ }
+ }
+ if (selectedMechanism != null) {
+ // A SASL mechanism was found. Authenticate using the selected mechanism and then
+ // proceed to bind a resource
+ try {
+ Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
+ Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);
+ currentMechanism = constructor.newInstance(this);
+ // Trigger SASL authentication with the selected mechanism. We use
+ // connection.getHost() since GSAPI requires the FQDN of the server, which
+ // may not match the XMPP domain.
+ currentMechanism.authenticate(username, connection.getServiceName(), password);
+
+ // Wait until SASL negotiation finishes
+ synchronized (this) {
+ if (!saslNegotiated && !saslFailed) {
+ try {
+ wait(30000);
+ }
+ catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+ }
+
+ if (saslFailed) {
+ // SASL authentication failed and the server may have closed the connection
+ // so throw an exception
+ if (errorCondition != null) {
+ throw new XMPPException("SASL authentication " +
+ selectedMechanism + " failed: " + errorCondition);
+ }
+ else {
+ throw new XMPPException("SASL authentication failed using mechanism " +
+ selectedMechanism);
+ }
+ }
+
+ 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,"");
+
+ // Wait until SASL negotiation finishes
+ synchronized (this) {
+ if (!saslNegotiated && !saslFailed) {
+ try {
+ wait(5000);
+ }
+ catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+ }
+
+ if (saslFailed) {
+ // SASL authentication failed and the server may have closed the connection
+ // so throw an exception
+ if (errorCondition != null) {
+ throw new XMPPException("SASL authentication failed: " + errorCondition);
+ }
+ else {
+ 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) {
+ // Ignore
+ }
+ }
+ }
+
+ 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());
+ }
+ }
+ 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<String> 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.
+ *
+ * @deprecated replaced by {@see #authenticationFailed(String)}.
+ */
+ void authenticationFailed() {
+ authenticationFailed(null);
+ }
+
+ /**
+ * Notification message saying that SASL authentication has failed. The server may have
+ * closed the connection depending on the number of possible retries.
+ *
+ * @param condition the error condition provided by the server.
+ */
+ void authenticationFailed(String condition) {
+ synchronized (this) {
+ saslFailed = true;
+ errorCondition = condition;
+ // 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(Packet stanza) {
+ connection.sendPacket(stanza);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Initializes the internal state in order to be able to be reused. The authentication
+ * is used by the connection at the first login and then reused after the connection
+ * is disconnected and then reconnected.
+ */
+ protected void init() {
+ saslNegotiated = false;
+ saslFailed = false;
+ resourceBinded = false;
+ sessionSupported = false;
+ }
+}
diff --git a/src/org/jivesoftware/smack/SASLAuthentication.java.orig b/src/org/jivesoftware/smack/SASLAuthentication.java.orig
new file mode 100644
index 0000000..66ff693
--- /dev/null
+++ b/src/org/jivesoftware/smack/SASLAuthentication.java.orig
@@ -0,0 +1,586 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.Packet;
+import org.jivesoftware.smack.packet.Session;
+import org.jivesoftware.smack.sasl.*;
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.util.*;
+
+/**
+ * <p>This class is responsible authenticating the user using SASL, binding the resource
+ * to the connection and establishing a session with the server.</p>
+ *
+ * <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>
+ *
+ * <p>The server may support many SASL mechanisms to use for authenticating. Out of the box
+ * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use
+ * {@link #registerSASLMechanism(String, Class)} to register a new mechanisms. A registered
+ * mechanism wont be used until {@link #supportSASLMechanism(String, int)} is called. By default,
+ * the list of supported SASL mechanisms is determined from the {@link SmackConfiguration}. </p>
+ *
+ * <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>
+ *
+ * <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.</p>
+ *
+ * @see org.jivesoftware.smack.sasl.SASLMechanism
+ *
+ * @author Gaston Dombiak
+ * @author Jay Kline
+ */
+public class SASLAuthentication implements UserAuthentication {
+
+ private static Map<String, Class<? extends SASLMechanism>> implementedMechanisms = new HashMap<String, Class<? extends SASLMechanism>>();
+ private static List<String> mechanismsPreferences = new ArrayList<String>();
+
+ private Connection connection;
+ private Collection<String> serverMechanisms = new ArrayList<String>();
+ private SASLMechanism currentMechanism = null;
+ /**
+ * Boolean indicating if SASL negotiation has finished and was successful.
+ */
+ private boolean saslNegotiated;
+ /**
+ * Boolean indication if SASL authentication has failed. When failed the server may end
+ * the connection.
+ */
+ private boolean saslFailed;
+ private boolean resourceBinded;
+ private boolean sessionSupported;
+ /**
+ * The SASL related error condition if there was one provided by the server.
+ */
+ private String errorCondition;
+
+ static {
+
+ // Register SASL mechanisms supported by Smack
+ registerSASLMechanism("EXTERNAL", SASLExternalMechanism.class);
+ registerSASLMechanism("GSSAPI", SASLGSSAPIMechanism.class);
+ registerSASLMechanism("DIGEST-MD5", SASLDigestMD5Mechanism.class);
+ registerSASLMechanism("CRAM-MD5", SASLCramMD5Mechanism.class);
+ registerSASLMechanism("PLAIN", SASLPlainMechanism.class);
+ registerSASLMechanism("ANONYMOUS", SASLAnonymous.class);
+
+ supportSASLMechanism("GSSAPI",0);
+ supportSASLMechanism("DIGEST-MD5",1);
+ supportSASLMechanism("CRAM-MD5",2);
+ supportSASLMechanism("PLAIN",3);
+ supportSASLMechanism("ANONYMOUS",4);
+
+ }
+
+ /**
+ * Registers a new SASL mechanism
+ *
+ * @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(String name, Class<? extends SASLMechanism> mClass) {
+ implementedMechanisms.put(name, mClass);
+ }
+
+ /**
+ * Unregisters an existing SASL mechanism. Once the mechanism has been unregistered it won't
+ * be possible to authenticate users using the removed SASL mechanism. It also removes the
+ * mechanism from the supported list.
+ *
+ * @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);
+ }
+
+
+ /**
+ * 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.
+ * The SASL mechanism must be registered via {@link #registerSASLMechanism(String, Class)}
+ *
+ * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
+ */
+ public static void supportSASLMechanism(String name) {
+ mechanismsPreferences.add(0, name);
+ }
+
+ /**
+ * 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.
+ * 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. The SASL mechanism must be
+ * registered via {@link #registerSASLMechanism(String, Class)}
+ *
+ * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
+ * @param index preference position amongst all the implemented SASL mechanism. Starts with 0.
+ */
+ public static void supportSASLMechanism(String name, int index) {
+ mechanismsPreferences.add(index, name);
+ }
+
+ /**
+ * Un-supports an existing SASL mechanism. Once the mechanism has been unregistered it won't
+ * be possible to authenticate users using the removed SASL mechanism. Note that the mechanism
+ * is still registered, but will just not be used.
+ *
+ * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.
+ */
+ public static void unsupportSASLMechanism(String 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<Class<? extends SASLMechanism>> getRegisterSASLMechanisms() {
+ List<Class<? extends SASLMechanism>> answer = new ArrayList<Class<? extends SASLMechanism>>();
+ for (String mechanismsPreference : mechanismsPreferences) {
+ answer.add(implementedMechanisms.get(mechanismsPreference));
+ }
+ return answer;
+ }
+
+ SASLAuthentication(Connection connection) {
+ super();
+ this.connection = connection;
+ this.init();
+ }
+
+ /**
+ * 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() {
+ return !serverMechanisms.isEmpty() && (serverMechanisms.size() != 1 || !hasAnonymousAuthentication());
+ }
+
+ /**
+ * 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 resource the desired resource.
+ * @param cbh the CallbackHandler used to get information from the user
+ * @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 resource, CallbackHandler cbh)
+ throws XMPPException {
+ // Locate the SASLMechanism to use
+ String selectedMechanism = null;
+ for (String mechanism : mechanismsPreferences) {
+ if (implementedMechanisms.containsKey(mechanism) &&
+ serverMechanisms.contains(mechanism)) {
+ selectedMechanism = mechanism;
+ break;
+ }
+ }
+ if (selectedMechanism != null) {
+ // A SASL mechanism was found. Authenticate using the selected mechanism and then
+ // proceed to bind a resource
+ try {
+ Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
+ Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);
+ currentMechanism = constructor.newInstance(this);
+ // Trigger SASL authentication with the selected mechanism. We use
+ // connection.getHost() since GSAPI requires the FQDN of the server, which
+ // may not match the XMPP domain.
+ currentMechanism.authenticate(username, connection.getHost(), cbh);
+
+ // Wait until SASL negotiation finishes
+ synchronized (this) {
+ if (!saslNegotiated && !saslFailed) {
+ try {
+ wait(30000);
+ }
+ catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+ }
+
+ if (saslFailed) {
+ // SASL authentication failed and the server may have closed the connection
+ // so throw an exception
+ if (errorCondition != null) {
+ throw new XMPPException("SASL authentication " +
+ selectedMechanism + " failed: " + errorCondition);
+ }
+ else {
+ throw new XMPPException("SASL authentication failed using mechanism " +
+ selectedMechanism);
+ }
+ }
+
+ if (saslNegotiated) {
+ // Bind a resource for this connection and
+ return bindResourceAndEstablishSession(resource);
+ } else {
+ // SASL authentication failed
+ }
+ }
+ catch (XMPPException e) {
+ throw e;
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ else {
+ throw new XMPPException("SASL Authentication failed. No known authentication mechanisims.");
+ }
+ throw new XMPPException("SASL authentication failed");
+ }
+
+ /**
+ * 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
+ String selectedMechanism = null;
+ for (String mechanism : mechanismsPreferences) {
+ if (implementedMechanisms.containsKey(mechanism) &&
+ serverMechanisms.contains(mechanism)) {
+ selectedMechanism = mechanism;
+ break;
+ }
+ }
+ if (selectedMechanism != null) {
+ // A SASL mechanism was found. Authenticate using the selected mechanism and then
+ // proceed to bind a resource
+ try {
+ Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);
+ Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);
+ currentMechanism = constructor.newInstance(this);
+ // Trigger SASL authentication with the selected mechanism. We use
+ // connection.getHost() since GSAPI requires the FQDN of the server, which
+ // may not match the XMPP domain.
+ currentMechanism.authenticate(username, connection.getServiceName(), password);
+
+ // Wait until SASL negotiation finishes
+ synchronized (this) {
+ if (!saslNegotiated && !saslFailed) {
+ try {
+ wait(30000);
+ }
+ catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+ }
+
+ if (saslFailed) {
+ // SASL authentication failed and the server may have closed the connection
+ // so throw an exception
+ if (errorCondition != null) {
+ throw new XMPPException("SASL authentication " +
+ selectedMechanism + " failed: " + errorCondition);
+ }
+ else {
+ throw new XMPPException("SASL authentication failed using mechanism " +
+ selectedMechanism);
+ }
+ }
+
+ 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,"");
+
+ // Wait until SASL negotiation finishes
+ synchronized (this) {
+ if (!saslNegotiated && !saslFailed) {
+ try {
+ wait(5000);
+ }
+ catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+ }
+
+ if (saslFailed) {
+ // SASL authentication failed and the server may have closed the connection
+ // so throw an exception
+ if (errorCondition != null) {
+ throw new XMPPException("SASL authentication failed: " + errorCondition);
+ }
+ else {
+ 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) {
+ // Ignore
+ }
+ }
+ }
+
+ 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());
+ }
+ }
+ 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<String> 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.
+ *
+ * @deprecated replaced by {@see #authenticationFailed(String)}.
+ */
+ void authenticationFailed() {
+ authenticationFailed(null);
+ }
+
+ /**
+ * Notification message saying that SASL authentication has failed. The server may have
+ * closed the connection depending on the number of possible retries.
+ *
+ * @param condition the error condition provided by the server.
+ */
+ void authenticationFailed(String condition) {
+ synchronized (this) {
+ saslFailed = true;
+ errorCondition = condition;
+ // 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(Packet stanza) {
+ connection.sendPacket(stanza);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Initializes the internal state in order to be able to be reused. The authentication
+ * is used by the connection at the first login and then reused after the connection
+ * is disconnected and then reconnected.
+ */
+ protected void init() {
+ saslNegotiated = false;
+ saslFailed = false;
+ resourceBinded = false;
+ sessionSupported = false;
+ }
+}
diff --git a/src/org/jivesoftware/smack/ServerTrustManager.java b/src/org/jivesoftware/smack/ServerTrustManager.java
new file mode 100644
index 0000000..63da3e7
--- /dev/null
+++ b/src/org/jivesoftware/smack/ServerTrustManager.java
@@ -0,0 +1,331 @@
+/**
+ * $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.io.InputStream;
+import java.io.IOException;
+import java.security.*;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 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 static Pattern cnPattern = Pattern.compile("(?i)(cn=)([^,]*)");
+
+ private ConnectionConfiguration configuration;
+
+ /**
+ * Holds the domain of the remote server we are trying to connect
+ */
+ private String server;
+ private KeyStore trustStore;
+
+ private static Map<KeyStoreOptions, KeyStore> stores = new HashMap<KeyStoreOptions, KeyStore>();
+
+ public ServerTrustManager(String server, ConnectionConfiguration configuration) {
+ this.configuration = configuration;
+ this.server = server;
+
+ InputStream in = null;
+ synchronized (stores) {
+ KeyStoreOptions options = new KeyStoreOptions(configuration.getTruststoreType(),
+ configuration.getTruststorePath(), configuration.getTruststorePassword());
+ if (stores.containsKey(options)) {
+ trustStore = stores.get(options);
+ } else {
+ try {
+ trustStore = KeyStore.getInstance(options.getType());
+ in = new FileInputStream(options.getPath());
+ trustStore.load(in, options.getPassword().toCharArray());
+ } catch (Exception e) {
+ trustStore = null;
+ e.printStackTrace();
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ioe) {
+ // Ignore.
+ }
+ }
+ }
+ stores.put(options, trustStore);
+ }
+ if (trustStore == null)
+ // 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;
+
+ List<String> peerIdentities = 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 " + peerIdentities);
+ }
+ }
+ else {
+ throw new CertificateException(
+ "subject/issuer verification failed of " + peerIdentities);
+ }
+ }
+ 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: " +
+ peerIdentities);
+ trusted = true;
+ }
+ }
+ catch (KeyStoreException e) {
+ e.printStackTrace();
+ }
+ if (!trusted) {
+ throw new CertificateException("root certificate not trusted of " + peerIdentities);
+ }
+ }
+
+ 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 (peerIdentities.size() == 1 && peerIdentities.get(0).startsWith("*.")) {
+ // Remove the wildcard
+ String peerIdentity = peerIdentities.get(0).replace("*.", "");
+ // Check if the requested subdomain matches the certified domain
+ if (!server.endsWith(peerIdentity)) {
+ throw new CertificateException("target verification failed of " + peerIdentities);
+ }
+ }
+ else if (!peerIdentities.contains(server)) {
+ throw new CertificateException("target verification failed of " + peerIdentities);
+ }
+ }
+
+ 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 List<String> getPeerIdentity(X509Certificate x509Certificate) {
+ // Look the identity in the subjectAltName extension if available
+ List<String> names = getSubjectAlternativeNames(x509Certificate);
+ if (names.isEmpty()) {
+ String name = x509Certificate.getSubjectDN().getName();
+ Matcher matcher = cnPattern.matcher(name);
+ if (matcher.find()) {
+ name = matcher.group(2);
+ }
+ // Create an array with the unique identity
+ names = new ArrayList<String>();
+ names.add(name);
+ }
+ return names;
+ }
+
+ /**
+ * Returns the JID representation of an XMPP entity contained as a SubjectAltName extension
+ * in the certificate. If none was found then return <tt>null</tt>.
+ *
+ * @param certificate the certificate presented by the remote entity.
+ * @return the JID representation of an XMPP entity contained as a SubjectAltName extension
+ * in the certificate. If none was found then return <tt>null</tt>.
+ */
+ private static List<String> getSubjectAlternativeNames(X509Certificate certificate) {
+ List<String> identities = new ArrayList<String>();
+ try {
+ Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
+ // Check that the certificate includes the SubjectAltName extension
+ if (altNames == null) {
+ return Collections.emptyList();
+ }
+ // Use the type OtherName to search for the certified server name
+ /*for (List item : altNames) {
+ Integer type = (Integer) item.get(0);
+ if (type == 0) {
+ // Type OtherName found so return the associated value
+ try {
+ // Value is encoded using ASN.1 so decode it to get the server's identity
+ ASN1InputStream decoder = new ASN1InputStream((byte[]) item.toArray()[1]);
+ DEREncodable encoded = decoder.readObject();
+ encoded = ((DERSequence) encoded).getObjectAt(1);
+ encoded = ((DERTaggedObject) encoded).getObject();
+ encoded = ((DERTaggedObject) encoded).getObject();
+ String identity = ((DERUTF8String) encoded).getString();
+ // Add the decoded server name to the list of identities
+ identities.add(identity);
+ }
+ catch (UnsupportedEncodingException e) {
+ // Ignore
+ }
+ catch (IOException e) {
+ // Ignore
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ // Other types are not good for XMPP so ignore them
+ System.out.println("SubjectAltName of invalid type found: " + certificate);
+ }*/
+ }
+ catch (CertificateParsingException e) {
+ e.printStackTrace();
+ }
+ return identities;
+ }
+
+ private static class KeyStoreOptions {
+ private final String type;
+ private final String path;
+ private final String password;
+
+ public KeyStoreOptions(String type, String path, String password) {
+ super();
+ this.type = type;
+ this.path = path;
+ this.password = password;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((password == null) ? 0 : password.hashCode());
+ result = prime * result + ((path == null) ? 0 : path.hashCode());
+ result = prime * result + ((type == null) ? 0 : type.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ KeyStoreOptions other = (KeyStoreOptions) obj;
+ if (password == null) {
+ if (other.password != null)
+ return false;
+ } else if (!password.equals(other.password))
+ return false;
+ if (path == null) {
+ if (other.path != null)
+ return false;
+ } else if (!path.equals(other.path))
+ return false;
+ if (type == null) {
+ if (other.type != null)
+ return false;
+ } else if (!type.equals(other.type))
+ return false;
+ return true;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/SmackAndroid.java b/src/org/jivesoftware/smack/SmackAndroid.java
new file mode 100644
index 0000000..a18d675
--- /dev/null
+++ b/src/org/jivesoftware/smack/SmackAndroid.java
@@ -0,0 +1,59 @@
+package org.jivesoftware.smack;
+
+import org.jivesoftware.smack.util.DNSUtil;
+import org.jivesoftware.smack.util.dns.DNSJavaResolver;
+import org.jivesoftware.smackx.ConfigureProviderManager;
+import org.jivesoftware.smackx.InitStaticCode;
+import org.xbill.DNS.ResolverConfig;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+public class SmackAndroid {
+ private static SmackAndroid sSmackAndroid = null;
+
+ private BroadcastReceiver mConnectivityChangedReceiver;
+ private Context mCtx;
+
+ private SmackAndroid(Context ctx) {
+ mCtx = ctx;
+ DNSUtil.setDNSResolver(DNSJavaResolver.getInstance());
+ InitStaticCode.initStaticCode(ctx);
+ ConfigureProviderManager.configureProviderManager();
+ maybeRegisterReceiver();
+ }
+
+ public static SmackAndroid init(Context ctx) {
+ if (sSmackAndroid == null) {
+ sSmackAndroid = new SmackAndroid(ctx);
+ } else {
+ sSmackAndroid.maybeRegisterReceiver();
+ }
+ return sSmackAndroid;
+ }
+
+ public void onDestroy() {
+ if (mConnectivityChangedReceiver != null) {
+ mCtx.unregisterReceiver(mConnectivityChangedReceiver);
+ mConnectivityChangedReceiver = null;
+ }
+ }
+
+ private void maybeRegisterReceiver() {
+ if (mConnectivityChangedReceiver == null) {
+ mConnectivityChangedReceiver = new ConnectivtyChangedReceiver();
+ mCtx.registerReceiver(mConnectivityChangedReceiver, new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE"));
+ }
+ }
+
+ class ConnectivtyChangedReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ResolverConfig.refresh();
+ }
+
+ }
+}
diff --git a/src/org/jivesoftware/smack/SmackConfiguration.java b/src/org/jivesoftware/smack/SmackConfiguration.java
new file mode 100644
index 0000000..2696d87
--- /dev/null
+++ b/src/org/jivesoftware/smack/SmackConfiguration.java
@@ -0,0 +1,371 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Vector;
+
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * 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 = "3.2.2";
+
+ private static int packetReplyTimeout = 5000;
+ private static Vector<String> defaultMechs = new Vector<String>();
+
+ private static boolean localSocks5ProxyEnabled = true;
+ private static int localSocks5ProxyPort = 7777;
+ private static int packetCollectorSize = 5000;
+
+ /**
+ * defaultPingInterval (in seconds)
+ */
+ private static int defaultPingInterval = 1800; // 30 min (30*60)
+
+ /**
+ * This automatically enables EntityCaps for new connections if it is set to true
+ */
+ private static boolean autoEnableEntityCaps = false;
+
+ 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 (ClassLoader classLoader : classLoaders) {
+ Enumeration<URL> configEnum = classLoader.getResources("META-INF/smack-config.xml");
+ while (configEnum.hasMoreElements()) {
+ URL url = configEnum.nextElement();
+ InputStream systemStream = null;
+ try {
+ systemStream = url.openStream();
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ 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("mechName")) {
+ defaultMechs.add(parser.nextText());
+ }
+ else if (parser.getName().equals("localSocks5ProxyEnabled")) {
+ localSocks5ProxyEnabled = Boolean.parseBoolean(parser.nextText());
+ }
+ else if (parser.getName().equals("localSocks5ProxyPort")) {
+ localSocks5ProxyPort = parseIntProperty(parser, localSocks5ProxyPort);
+ }
+ else if (parser.getName().equals("packetCollectorSize")) {
+ packetCollectorSize = parseIntProperty(parser, packetCollectorSize);
+ }
+ else if (parser.getName().equals("defaultPingInterval")) {
+ defaultPingInterval = parseIntProperty(parser, defaultPingInterval);
+ }
+ else if (parser.getName().equals("autoEnableEntityCaps")) {
+ autoEnableEntityCaps = Boolean.parseBoolean(parser.nextText());
+ }
+ }
+ eventType = parser.next();
+ }
+ while (eventType != XmlPullParser.END_DOCUMENT);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ finally {
+ try {
+ systemStream.close();
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+ }
+ }
+ }
+ }
+ 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;
+ }
+
+ /**
+ * Gets the default max size of a packet collector before it will delete
+ * the older packets.
+ *
+ * @return The number of packets to queue before deleting older packets.
+ */
+ public static int getPacketCollectorSize() {
+ return packetCollectorSize;
+ }
+
+ /**
+ * Sets the default max size of a packet collector before it will delete
+ * the older packets.
+ *
+ * @param The number of packets to queue before deleting older packets.
+ */
+ public static void setPacketCollectorSize(int collectorSize) {
+ packetCollectorSize = collectorSize;
+ }
+
+ /**
+ * Add a SASL mechanism to the list to be used.
+ *
+ * @param mech the SASL mechanism to be added
+ */
+ public static void addSaslMech(String mech) {
+ if(! defaultMechs.contains(mech) ) {
+ defaultMechs.add(mech);
+ }
+ }
+
+ /**
+ * Add a Collection of SASL mechanisms to the list to be used.
+ *
+ * @param mechs the Collection of SASL mechanisms to be added
+ */
+ public static void addSaslMechs(Collection<String> mechs) {
+ for(String mech : mechs) {
+ addSaslMech(mech);
+ }
+ }
+
+ /**
+ * Remove a SASL mechanism from the list to be used.
+ *
+ * @param mech the SASL mechanism to be removed
+ */
+ public static void removeSaslMech(String mech) {
+ if( defaultMechs.contains(mech) ) {
+ defaultMechs.remove(mech);
+ }
+ }
+
+ /**
+ * Remove a Collection of SASL mechanisms to the list to be used.
+ *
+ * @param mechs the Collection of SASL mechanisms to be removed
+ */
+ public static void removeSaslMechs(Collection<String> mechs) {
+ for(String mech : mechs) {
+ removeSaslMech(mech);
+ }
+ }
+
+ /**
+ * Returns the list of SASL mechanisms to be used. If a SASL mechanism is
+ * listed here it does not guarantee it will be used. The server may not
+ * support it, or it may not be implemented.
+ *
+ * @return the list of SASL mechanisms to be used.
+ */
+ public static List<String> getSaslMechs() {
+ return defaultMechs;
+ }
+
+ /**
+ * Returns true if the local Socks5 proxy should be started. Default is true.
+ *
+ * @return if the local Socks5 proxy should be started
+ */
+ public static boolean isLocalSocks5ProxyEnabled() {
+ return localSocks5ProxyEnabled;
+ }
+
+ /**
+ * Sets if the local Socks5 proxy should be started. Default is true.
+ *
+ * @param localSocks5ProxyEnabled if the local Socks5 proxy should be started
+ */
+ public static void setLocalSocks5ProxyEnabled(boolean localSocks5ProxyEnabled) {
+ SmackConfiguration.localSocks5ProxyEnabled = localSocks5ProxyEnabled;
+ }
+
+ /**
+ * Return the port of the local Socks5 proxy. Default is 7777.
+ *
+ * @return the port of the local Socks5 proxy
+ */
+ public static int getLocalSocks5ProxyPort() {
+ return localSocks5ProxyPort;
+ }
+
+ /**
+ * Sets the port of the local Socks5 proxy. Default is 7777. If you set the port to a negative
+ * value Smack tries the absolute value and all following until it finds an open port.
+ *
+ * @param localSocks5ProxyPort the port of the local Socks5 proxy to set
+ */
+ public static void setLocalSocks5ProxyPort(int localSocks5ProxyPort) {
+ SmackConfiguration.localSocks5ProxyPort = localSocks5ProxyPort;
+ }
+
+ /**
+ * Returns the default ping interval (seconds)
+ *
+ * @return
+ */
+ public static int getDefaultPingInterval() {
+ return defaultPingInterval;
+ }
+
+ /**
+ * Sets the default ping interval (seconds). Set it to '-1' to disable the periodic ping
+ *
+ * @param defaultPingInterval
+ */
+ public static void setDefaultPingInterval(int defaultPingInterval) {
+ SmackConfiguration.defaultPingInterval = defaultPingInterval;
+ }
+
+ /**
+ * Check if Entity Caps are enabled as default for every new connection
+ * @return
+ */
+ public static boolean autoEnableEntityCaps() {
+ return autoEnableEntityCaps;
+ }
+
+ /**
+ * Set if Entity Caps are enabled or disabled for every new connection
+ *
+ * @param true if Entity Caps should be auto enabled, false if not
+ */
+ public static void setAutoEnableEntityCaps(boolean b) {
+ autoEnableEntityCaps = b;
+ }
+
+ 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] = SmackConfiguration.class.getClassLoader();
+ classLoaders[1] = Thread.currentThread().getContextClassLoader();
+ // Clean up possible null values. Note that #getClassLoader may return a null value.
+ List<ClassLoader> loaders = new ArrayList<ClassLoader>();
+ for (ClassLoader classLoader : classLoaders) {
+ if (classLoader != null) {
+ loaders.add(classLoader);
+ }
+ }
+ return loaders.toArray(new ClassLoader[loaders.size()]);
+ }
+}
diff --git a/src/org/jivesoftware/smack/UserAuthentication.java b/src/org/jivesoftware/smack/UserAuthentication.java
new file mode 100644
index 0000000..38b30ca
--- /dev/null
+++ b/src/org/jivesoftware/smack/UserAuthentication.java
@@ -0,0 +1,79 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.apache.harmony.javax.security.auth.callback.CallbackHandler;
+
+/**
+ * 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
+ * @author Jay Kline
+ */
+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
+ * requested by this method.
+ *
+ * Note that using callbacks is the prefered method of authenticating users since it allows
+ * more flexability in the mechanisms used.
+ *
+ * @param username the requested username (authorization ID) for authenticating to the server
+ * @param resource the requested resource.
+ * @param cbh the CallbackHandler used to obtain authentication ID, password, or other
+ * information
+ * @return the full JID provided by the server while binding a resource for the connection.
+ * @throws XMPPException if an error occurs while authenticating.
+ */
+ String authenticate(String username, String resource, CallbackHandler cbh) throws
+ XMPPException;
+
+ /**
+ * 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.
+ *
+ * It is recommended that @{link #authenticate(String, String, CallbackHandler)} be used instead
+ * since it provides greater flexability in authenticaiton and authorization.
+ *
+ * @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/src/org/jivesoftware/smack/XMPPConnection.java b/src/org/jivesoftware/smack/XMPPConnection.java
new file mode 100644
index 0000000..badf29c
--- /dev/null
+++ b/src/org/jivesoftware/smack/XMPPConnection.java
@@ -0,0 +1,1116 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.compression.XMPPInputOutputStream;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smack.util.dns.HostAddress;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.Constructor;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyStore;
+import java.security.Provider;
+import java.security.Security;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Creates a socket connection to a XMPP server. This is the default connection
+ * to a Jabber server and is specified in the XMPP Core (RFC 3920).
+ *
+ * @see Connection
+ * @author Matt Tucker
+ */
+public class XMPPConnection extends Connection {
+
+ /**
+ * The socket which is used for this connection.
+ */
+ Socket socket;
+
+ String connectionID = null;
+ private String user = null;
+ private boolean connected = false;
+ // socketClosed is used concurrent
+ // by XMPPConnection, PacketReader, PacketWriter
+ private volatile boolean socketClosed = false;
+
+ /**
+ * Flag that indicates if the user is currently authenticated with the server.
+ */
+ private boolean authenticated = false;
+ /**
+ * Flag that indicates if the user was authenticated with the server when the connection
+ * to the server was closed (abruptly or not).
+ */
+ private boolean wasAuthenticated = false;
+ private boolean anonymous = false;
+ private boolean usingTLS = false;
+
+ PacketWriter packetWriter;
+ PacketReader packetReader;
+
+ Roster roster = null;
+
+ /**
+ * Collection of available stream compression methods offered by the server.
+ */
+ private Collection<String> compressionMethods;
+
+ /**
+ * Set to true by packet writer if the server acknowledged the compression
+ */
+ private boolean serverAckdCompression = false;
+
+ /**
+ * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be
+ * performed to determine the IP address and port corresponding to the
+ * service name; if that lookup fails, it's assumed that server resides at
+ * <tt>serviceName</tt> with the default port of 5222. Encrypted connections (TLS)
+ * will be used if available, stream compression is disabled, and standard SASL
+ * mechanisms will be used for authentication.<p>
+ * <p/>
+ * This is the simplest constructor for connecting to an XMPP server. Alternatively,
+ * you can get fine-grained control over connection settings using the
+ * {@link #XMPPConnection(ConnectionConfiguration)} constructor.<p>
+ * <p/>
+ * Note that XMPPConnection constructors do not establish a connection to the server
+ * and you must call {@link #connect()}.<p>
+ * <p/>
+ * The CallbackHandler will only be used if the connection requires the client provide
+ * an SSL certificate to the server. The CallbackHandler must handle the PasswordCallback
+ * to prompt for a password to unlock the keystore containing the SSL certificate.
+ *
+ * @param serviceName the name of the XMPP server to connect to; e.g. <tt>example.com</tt>.
+ * @param callbackHandler the CallbackHandler used to prompt for the password to the keystore.
+ */
+ public XMPPConnection(String serviceName, CallbackHandler callbackHandler) {
+ // Create the configuration for this new connection
+ super(new ConnectionConfiguration(serviceName));
+ config.setCompressionEnabled(false);
+ config.setSASLAuthenticationEnabled(true);
+ config.setDebuggerEnabled(DEBUG_ENABLED);
+ config.setCallbackHandler(callbackHandler);
+ }
+
+ /**
+ * Creates a new XMPP connection in the same way {@link #XMPPConnection(String,CallbackHandler)} does, but
+ * with no callback handler for password prompting of the keystore. This will work
+ * in most cases, provided the client is not required to provide a certificate to
+ * the server.
+ *
+ * @param serviceName the name of the XMPP server to connect to; e.g. <tt>example.com</tt>.
+ */
+ public XMPPConnection(String serviceName) {
+ // Create the configuration for this new connection
+ super(new ConnectionConfiguration(serviceName));
+ config.setCompressionEnabled(false);
+ config.setSASLAuthenticationEnabled(true);
+ config.setDebuggerEnabled(DEBUG_ENABLED);
+ }
+
+ /**
+ * Creates a new XMPP connection in the same way {@link #XMPPConnection(ConnectionConfiguration,CallbackHandler)} does, but
+ * with no callback handler for password prompting of the keystore. This will work
+ * in most cases, provided the client is not required to provide a certificate to
+ * the server.
+ *
+ *
+ * @param config the connection configuration.
+ */
+ public XMPPConnection(ConnectionConfiguration config) {
+ super(config);
+ }
+
+ /**
+ * Creates a new XMPP connection using the specified connection configuration.<p>
+ * <p/>
+ * Manually specifying connection configuration information is suitable for
+ * advanced users of the API. In many cases, using the
+ * {@link #XMPPConnection(String)} constructor is a better approach.<p>
+ * <p/>
+ * Note that XMPPConnection constructors do not establish a connection to the server
+ * and you must call {@link #connect()}.<p>
+ * <p/>
+ *
+ * The CallbackHandler will only be used if the connection requires the client provide
+ * an SSL certificate to the server. The CallbackHandler must handle the PasswordCallback
+ * to prompt for a password to unlock the keystore containing the SSL certificate.
+ *
+ * @param config the connection configuration.
+ * @param callbackHandler the CallbackHandler used to prompt for the password to the keystore.
+ */
+ public XMPPConnection(ConnectionConfiguration config, CallbackHandler callbackHandler) {
+ super(config);
+ config.setCallbackHandler(callbackHandler);
+ }
+
+ public String getConnectionID() {
+ if (!isConnected()) {
+ return null;
+ }
+ return connectionID;
+ }
+
+ public String getUser() {
+ if (!isAuthenticated()) {
+ return null;
+ }
+ return user;
+ }
+
+ @Override
+ public synchronized void login(String username, String password, String resource) 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;
+ if (config.isSASLAuthenticationEnabled() &&
+ saslAuthentication.hasNonAnonymousAuthentication()) {
+ // Authenticate using SASL
+ if (password != null) {
+ response = saslAuthentication.authenticate(username, password, resource);
+ }
+ else {
+ response = saslAuthentication
+ .authenticate(username, resource, config.getCallbackHandler());
+ }
+ }
+ 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
+ config.setServiceName(StringUtils.parseServer(response));
+ }
+ else {
+ this.user = username + "@" + getServiceName();
+ if (resource != null) {
+ this.user += "/" + resource;
+ }
+ }
+
+ // If compression is enabled then request the server to use stream compression
+ if (config.isCompressionEnabled()) {
+ useCompression();
+ }
+
+ // Indicate that we're now authenticated.
+ authenticated = true;
+ anonymous = false;
+
+ // Create the roster if it is not a reconnection or roster already created by getRoster()
+ if (this.roster == null) {
+ if(rosterStorage==null){
+ this.roster = new Roster(this);
+ }
+ else{
+ this.roster = new Roster(this,rosterStorage);
+ }
+ }
+ if (config.isRosterLoadedAtLogin()) {
+ this.roster.reload();
+ }
+
+ // Set presence to online.
+ if (config.isSendPresence()) {
+ packetWriter.sendPacket(new Presence(Presence.Type.available));
+ }
+
+ // Stores the authentication for future reconnection
+ config.setLoginInfo(username, password, resource);
+
+ // 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 (config.isDebuggerEnabled() && debugger != null) {
+ debugger.userHasLogged(user);
+ }
+ }
+
+ @Override
+ 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;
+ if (config.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
+ config.setServiceName(StringUtils.parseServer(response));
+
+ // If compression is enabled then request the server to use stream compression
+ if (config.isCompressionEnabled()) {
+ useCompression();
+ }
+
+ // Set presence to online.
+ packetWriter.sendPacket(new Presence(Presence.Type.available));
+
+ // Indicate that we're now authenticated.
+ authenticated = true;
+ 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 (config.isDebuggerEnabled() && debugger != null) {
+ debugger.userHasLogged(user);
+ }
+ }
+
+ public Roster getRoster() {
+ // synchronize against login()
+ synchronized(this) {
+ // if connection is authenticated the roster is already set by login()
+ // or a previous call to getRoster()
+ if (!isAuthenticated() || isAnonymous()) {
+ if (roster == null) {
+ roster = new Roster(this);
+ }
+ return roster;
+ }
+ }
+
+ if (!config.isRosterLoadedAtLogin()) {
+ roster.reload();
+ }
+ // 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;
+ }
+
+ public boolean isConnected() {
+ return connected;
+ }
+
+ public boolean isSecureConnection() {
+ return isUsingTLS();
+ }
+
+ public boolean isSocketClosed() {
+ return socketClosed;
+ }
+
+ public boolean isAuthenticated() {
+ return authenticated;
+ }
+
+ public boolean isAnonymous() {
+ return anonymous;
+ }
+
+ /**
+ * Closes the connection by setting presence to unavailable then closing the stream to
+ * the XMPP server. The shutdown logic will be used during a planned disconnection or when
+ * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's
+ * packet reader, packet writer, and {@link Roster} will not be removed; thus
+ * connection's state is kept.
+ *
+ * @param unavailablePresence the presence packet to send during shutdown.
+ */
+ protected void shutdown(Presence unavailablePresence) {
+ // Set presence to offline.
+ if (packetWriter != null) {
+ packetWriter.sendPacket(unavailablePresence);
+ }
+
+ this.setWasAuthenticated(authenticated);
+ authenticated = false;
+
+ if (packetReader != null) {
+ packetReader.shutdown();
+ }
+ if (packetWriter != null) {
+ packetWriter.shutdown();
+ }
+
+ // Wait 150 ms for processes to clean-up, then shutdown.
+ try {
+ Thread.sleep(150);
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+
+ // Set socketClosed to true. This will cause the PacketReader
+ // and PacketWriter to ingore any Exceptions that are thrown
+ // because of a read/write from/to a closed stream.
+ // It is *important* that this is done before socket.close()!
+ socketClosed = true;
+ try {
+ socket.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ // In most cases the close() should be successful, so set
+ // connected to false here.
+ connected = false;
+
+ // Close down the readers and writers.
+ if (reader != null) {
+ try {
+ // Should already be closed by the previous
+ // socket.close(). But just in case do it explicitly.
+ reader.close();
+ }
+ catch (Throwable ignore) { /* ignore */ }
+ reader = null;
+ }
+ if (writer != null) {
+ try {
+ // Should already be closed by the previous
+ // socket.close(). But just in case do it explicitly.
+ writer.close();
+ }
+ catch (Throwable ignore) { /* ignore */ }
+ writer = null;
+ }
+
+ // Make sure that the socket is really closed
+ try {
+ // Does nothing if the socket is already closed
+ socket.close();
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+
+ saslAuthentication.init();
+ }
+
+ public synchronized void disconnect(Presence unavailablePresence) {
+ // If not connected, ignore this request.
+ PacketReader packetReader = this.packetReader;
+ PacketWriter packetWriter = this.packetWriter;
+ if (packetReader == null || packetWriter == null) {
+ return;
+ }
+
+ if (!isConnected()) {
+ return;
+ }
+
+ shutdown(unavailablePresence);
+
+ if (roster != null) {
+ roster.cleanup();
+ roster = null;
+ }
+ chatManager = null;
+
+ wasAuthenticated = false;
+
+ packetWriter.cleanup();
+ packetReader.cleanup();
+ }
+
+ 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 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.
+ * @deprecated replaced by {@link Connection#addPacketInterceptor(PacketInterceptor, PacketFilter)}.
+ */
+ public void addPacketWriterInterceptor(PacketInterceptor packetInterceptor,
+ PacketFilter packetFilter) {
+ addPacketInterceptor(packetInterceptor, packetFilter);
+ }
+
+ /**
+ * Removes a packet interceptor.
+ *
+ * @param packetInterceptor the packet interceptor to remove.
+ * @deprecated replaced by {@link Connection#removePacketInterceptor(PacketInterceptor)}.
+ */
+ public void removePacketWriterInterceptor(PacketInterceptor packetInterceptor) {
+ removePacketInterceptor(packetInterceptor);
+ }
+
+ /**
+ * 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.
+ * @deprecated replaced by {@link #addPacketSendingListener(PacketListener, PacketFilter)}.
+ */
+ public void addPacketWriterListener(PacketListener packetListener, PacketFilter packetFilter) {
+ addPacketSendingListener(packetListener, packetFilter);
+ }
+
+ /**
+ * Removes a packet listener for sending packets from this connection.
+ *
+ * @param packetListener the packet listener to remove.
+ * @deprecated replaced by {@link #removePacketSendingListener(PacketListener)}.
+ */
+ public void removePacketWriterListener(PacketListener packetListener) {
+ removePacketSendingListener(packetListener);
+ }
+
+ private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException {
+ XMPPException exception = null;
+ Iterator<HostAddress> it = config.getHostAddresses().iterator();
+ List<HostAddress> failedAddresses = new LinkedList<HostAddress>();
+ boolean xmppIOError = false;
+ while (it.hasNext()) {
+ exception = null;
+ HostAddress hostAddress = it.next();
+ String host = hostAddress.getFQDN();
+ int port = hostAddress.getPort();
+ try {
+ if (config.getSocketFactory() == null) {
+ this.socket = new Socket(host, port);
+ }
+ else {
+ this.socket = config.getSocketFactory().createSocket(host, port);
+ }
+ } catch (UnknownHostException uhe) {
+ String errorMessage = "Could not connect to " + host + ":" + port + ".";
+ exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_timeout,
+ errorMessage), uhe);
+ } catch (IOException ioe) {
+ String errorMessage = "XMPPError connecting to " + host + ":" + port + ".";
+ exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_error,
+ errorMessage), ioe);
+ xmppIOError = true;
+ }
+ if (exception == null) {
+ // We found a host to connect to, break here
+ config.setUsedHostAddress(hostAddress);
+ break;
+ }
+ hostAddress.setException(exception);
+ failedAddresses.add(hostAddress);
+ if (!it.hasNext()) {
+ // There are no more host addresses to try
+ // throw an exception and report all tried
+ // HostAddresses in the exception
+ StringBuilder sb = new StringBuilder();
+ for (HostAddress fha : failedAddresses) {
+ sb.append(fha.getErrorMessage());
+ sb.append("; ");
+ }
+ XMPPError xmppError;
+ if (xmppIOError) {
+ xmppError = new XMPPError(XMPPError.Condition.remote_server_error);
+ }
+ else {
+ xmppError = new XMPPError(XMPPError.Condition.remote_server_timeout);
+ }
+ throw new XMPPException(sb.toString(), xmppError);
+ }
+ }
+ socketClosed = false;
+ initConnection();
+ }
+
+ /**
+ * 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 initConnection() throws XMPPException {
+ boolean isFirstInitialization = packetReader == null || packetWriter == null;
+ compressionHandler = null;
+ serverAckdCompression = false;
+
+ // Set the reader and writer instance variables
+ initReaderAndWriter();
+
+ try {
+ if (isFirstInitialization) {
+ 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 (config.isDebuggerEnabled()) {
+ addPacketListener(debugger.getReaderListener(), null);
+ if (debugger.getWriterListener() != null) {
+ addPacketSendingListener(debugger.getWriterListener(), null);
+ }
+ }
+ }
+ else {
+ packetWriter.init();
+ packetReader.init();
+ }
+
+ // 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;
+
+ if (isFirstInitialization) {
+ // Notify listeners that a new connection has been established
+ for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
+ listener.connectionCreated(this);
+ }
+ }
+ else if (!wasAuthenticated) {
+ notifyReconnection();
+ }
+
+ }
+ 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;
+ }
+ this.setWasAuthenticated(authenticated);
+ chatManager = null;
+ authenticated = false;
+ connected = false;
+
+ throw ex; // Everything stoppped. Now throw the exception.
+ }
+ }
+
+ private void initReaderAndWriter() throws XMPPException {
+ try {
+ if (compressionHandler == null) {
+ reader =
+ new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
+ writer = new BufferedWriter(
+ new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
+ }
+ else {
+ try {
+ OutputStream os = compressionHandler.getOutputStream(socket.getOutputStream());
+ writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
+
+ InputStream is = compressionHandler.getInputStream(socket.getInputStream());
+ reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ compressionHandler = null;
+ 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(XMPPError.Condition.remote_server_error,
+ "XMPPError establishing connection with server."),
+ ioe);
+ }
+
+ // If debugging is enabled, we open a window and write out all network traffic.
+ initDebugger();
+ }
+
+ /***********************************************
+ * 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;
+ }
+
+ /**
+ * Notification message saying that the server supports TLS so confirm the server that we
+ * want to secure the connection.
+ *
+ * @param required true when the server indicates that TLS is required.
+ */
+ void startTLSReceived(boolean required) {
+ if (required && config.getSecurityMode() ==
+ ConnectionConfiguration.SecurityMode.disabled) {
+ notifyConnectionError(new IllegalStateException(
+ "TLS required by server but not allowed by connection configuration"));
+ return;
+ }
+
+ if (config.getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) {
+ // 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) {
+ 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.
+ *
+ * @throws Exception if an exception occurs.
+ */
+ void proceedTLSReceived() throws Exception {
+ SSLContext context = this.config.getCustomSSLContext();
+ KeyStore ks = null;
+ KeyManager[] kms = null;
+ PasswordCallback pcb = null;
+
+ if(config.getCallbackHandler() == null) {
+ ks = null;
+ } else if (context == null) {
+ //System.out.println("Keystore type: "+configuration.getKeystoreType());
+ if(config.getKeystoreType().equals("NONE")) {
+ ks = null;
+ pcb = null;
+ }
+ else if(config.getKeystoreType().equals("PKCS11")) {
+ try {
+ Constructor<?> c = Class.forName("sun.security.pkcs11.SunPKCS11").getConstructor(InputStream.class);
+ String pkcs11Config = "name = SmartCard\nlibrary = "+config.getPKCS11Library();
+ ByteArrayInputStream config = new ByteArrayInputStream(pkcs11Config.getBytes());
+ Provider p = (Provider)c.newInstance(config);
+ Security.addProvider(p);
+ ks = KeyStore.getInstance("PKCS11",p);
+ pcb = new PasswordCallback("PKCS11 Password: ",false);
+ this.config.getCallbackHandler().handle(new Callback[]{pcb});
+ ks.load(null,pcb.getPassword());
+ }
+ catch (Exception e) {
+ ks = null;
+ pcb = null;
+ }
+ }
+ else if(config.getKeystoreType().equals("Apple")) {
+ ks = KeyStore.getInstance("KeychainStore","Apple");
+ ks.load(null,null);
+ //pcb = new PasswordCallback("Apple Keychain",false);
+ //pcb.setPassword(null);
+ }
+ else {
+ ks = KeyStore.getInstance(config.getKeystoreType());
+ try {
+ pcb = new PasswordCallback("Keystore Password: ",false);
+ config.getCallbackHandler().handle(new Callback[]{pcb});
+ ks.load(new FileInputStream(config.getKeystorePath()), pcb.getPassword());
+ }
+ catch(Exception e) {
+ ks = null;
+ pcb = null;
+ }
+ }
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
+ try {
+ if(pcb == null) {
+ kmf.init(ks,null);
+ } else {
+ kmf.init(ks,pcb.getPassword());
+ pcb.clearPassword();
+ }
+ kms = kmf.getKeyManagers();
+ } catch (NullPointerException npe) {
+ kms = null;
+ }
+ }
+
+ // Verify certificate presented by the server
+ if (context == null) {
+ context = SSLContext.getInstance("TLS");
+ context.init(kms, new javax.net.ssl.TrustManager[] { new ServerTrustManager(getServiceName(), config) },
+ new java.security.SecureRandom());
+ }
+ Socket plain = socket;
+ // Secure the plain connection
+ socket = context.getSocketFactory().createSocket(plain,
+ plain.getInetAddress().getHostAddress(), 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();
+ //if (((SSLSocket) socket).getWantClientAuth()) {
+ // System.err.println("Connection wants client auth");
+ //}
+ //else if (((SSLSocket) socket).getNeedClientAuth()) {
+ // System.err.println("Connection needs client auth");
+ //}
+ //else {
+ // System.err.println("Connection does not require client auth");
+ // }
+ // 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<String> methods) {
+ compressionMethods = methods;
+ }
+
+ /**
+ * Returns the compression handler that can be used for one compression methods offered by the server.
+ *
+ * @return a instance of XMPPInputOutputStream or null if no suitable instance was found
+ *
+ */
+ private XMPPInputOutputStream maybeGetCompressionHandler() {
+ if (compressionMethods != null) {
+ for (XMPPInputOutputStream handler : compressionHandlers) {
+ if (!handler.isSupported())
+ continue;
+
+ String method = handler.getCompressionMethod();
+ if (compressionMethods.contains(method))
+ return handler;
+ }
+ }
+ return null;
+ }
+
+ public boolean isUsingCompression() {
+ return compressionHandler != null && serverAckdCompression;
+ }
+
+ /**
+ * 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>
+ * <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>
+ * <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.");
+ }
+
+ if ((compressionHandler = maybeGetCompressionHandler()) != null) {
+ requestStreamCompression(compressionHandler.getCompressionMethod());
+ // Wait until compression is being used or a timeout happened
+ synchronized (this) {
+ try {
+ this.wait(SmackConfiguration.getPacketReplyTimeout() * 5);
+ }
+ catch (InterruptedException e) {
+ // Ignore.
+ }
+ }
+ return isUsingCompression();
+ }
+ 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(String method) {
+ try {
+ writer.write("<compress xmlns='http://jabber.org/protocol/compress'>");
+ writer.write("<method>" + method + "</method></compress>");
+ writer.flush();
+ }
+ catch (IOException e) {
+ notifyConnectionError(e);
+ }
+ }
+
+ /**
+ * Start using stream compression since the server has acknowledged stream compression.
+ *
+ * @throws Exception if there is an exception starting stream compression.
+ */
+ void startStreamCompression() throws Exception {
+ serverAckdCompression = 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();
+ }
+ }
+
+ /**
+ * Notifies the XMPP connection that stream compression was denied so that
+ * the connection process can proceed.
+ */
+ void streamCompressionDenied() {
+ synchronized (this) {
+ this.notify();
+ }
+ }
+
+ /**
+ * Establishes a connection to the XMPP server and performs an automatic login
+ * only if the previous connection state was logged (authenticated). It basically
+ * creates and maintains a socket connection to the server.<p>
+ * <p/>
+ * Listeners will be preserved from a previous connection if the reconnection
+ * occurs after an abrupt termination.
+ *
+ * @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
+ * appropriate error messages to end-users.
+ */
+ public void connect() throws XMPPException {
+ // Establishes the connection, readers and writers
+ connectUsingConfiguration(config);
+ // Automatically makes the login if the user was previously connected successfully
+ // to the server and the connection was terminated abruptly
+ if (connected && wasAuthenticated) {
+ // Make the login
+ if (isAnonymous()) {
+ // Make the anonymous login
+ loginAnonymously();
+ }
+ else {
+ login(config.getUsername(), config.getPassword(), config.getResource());
+ }
+ notifyReconnection();
+ }
+ }
+
+ /**
+ * Sets whether the connection has already logged in the server.
+ *
+ * @param wasAuthenticated true if the connection has already been authenticated.
+ */
+ private void setWasAuthenticated(boolean wasAuthenticated) {
+ if (!this.wasAuthenticated) {
+ this.wasAuthenticated = wasAuthenticated;
+ }
+ }
+
+ @Override
+ public void setRosterStorage(RosterStorage storage)
+ throws IllegalStateException {
+ if(roster!=null){
+ throw new IllegalStateException("Roster is already initialized");
+ }
+ this.rosterStorage = storage;
+ }
+
+ /**
+ * Sends out a notification that there was an error with the connection
+ * and closes the connection. Also prints the stack trace of the given exception
+ *
+ * @param e the exception that causes the connection close event.
+ */
+ synchronized void notifyConnectionError(Exception e) {
+ // Listeners were already notified of the exception, return right here.
+ if (packetReader.done && packetWriter.done) return;
+
+ packetReader.done = true;
+ packetWriter.done = true;
+ // Closes the connection temporary. A reconnection is possible
+ shutdown(new Presence(Presence.Type.unavailable));
+ // Print the stack trace to help catch the problem
+ e.printStackTrace();
+ // Notify connection listeners of the error.
+ for (ConnectionListener listener : getConnectionListeners()) {
+ try {
+ listener.connectionClosedOnError(e);
+ }
+ catch (Exception e2) {
+ // Catch and print any exception so we can recover
+ // from a faulty listener
+ e2.printStackTrace();
+ }
+ }
+ }
+
+
+ /**
+ * Sends a notification indicating that the connection was reconnected successfully.
+ */
+ protected void notifyReconnection() {
+ // Notify connection listeners of the reconnection.
+ for (ConnectionListener listener : getConnectionListeners()) {
+ try {
+ listener.reconnectionSuccessful();
+ }
+ catch (Exception e) {
+ // Catch and print any exception so we can recover
+ // from a faulty listener
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/XMPPException.java b/src/org/jivesoftware/smack/XMPPException.java
new file mode 100644
index 0000000..6da24c2
--- /dev/null
+++ b/src/org/jivesoftware/smack/XMPPException.java
@@ -0,0 +1,219 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.StreamError;
+import org.jivesoftware.smack.packet.XMPPError;
+
+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() {
+ StringBuilder buf = new StringBuilder();
+ 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/src/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java b/src/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java
new file mode 100644
index 0000000..dc50451
--- /dev/null
+++ b/src/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright 2013 Florian Schmaus
+ *
+ * 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.compression;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * This class provides XMPP "zlib" compression with the help of the Deflater class of the Java API. Note that the method
+ * needed is available since Java7, so it will only work with Java7 or higher (hence it's name).
+ *
+ * @author Florian Schmaus
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/zip/Deflater.html#deflate(byte[], int, int, int)">The
+ * required deflate() method</a>
+ *
+ */
+public class Java7ZlibInputOutputStream extends XMPPInputOutputStream {
+ private final static Method method;
+ private final static boolean supported;
+ private final static int compressionLevel = Deflater.DEFAULT_STRATEGY;
+
+ static {
+ Method m = null;
+ try {
+ m = Deflater.class.getMethod("deflate", byte[].class, int.class, int.class, int.class);
+ } catch (SecurityException e) {
+ } catch (NoSuchMethodException e) {
+ }
+ method = m;
+ supported = (method != null);
+ }
+
+ public Java7ZlibInputOutputStream() {
+ compressionMethod = "zlib";
+ }
+
+ @Override
+ public boolean isSupported() {
+ return supported;
+ }
+
+ @Override
+ public InputStream getInputStream(InputStream inputStream) {
+ return new InflaterInputStream(inputStream, new Inflater(), 512) {
+ /**
+ * Provide a more InputStream compatible version. A return value of 1 means that it is likely to read one
+ * byte without blocking, 0 means that the system is known to block for more input.
+ *
+ * @return 0 if no data is available, 1 otherwise
+ * @throws IOException
+ */
+ @Override
+ public int available() throws IOException {
+ /*
+ * aSmack related remark (where KXmlParser is used):
+ * This is one of the funny code blocks. InflaterInputStream.available violates the contract of
+ * InputStream.available, which breaks kXML2.
+ *
+ * I'm not sure who's to blame, oracle/sun for a broken api or the google guys for mixing a sun bug with
+ * a xml reader that can't handle it....
+ *
+ * Anyway, this simple if breaks suns distorted reality, but helps to use the api as intended.
+ */
+ if (inf.needsInput()) {
+ return 0;
+ }
+ return super.available();
+ }
+ };
+ }
+
+ @Override
+ public OutputStream getOutputStream(OutputStream outputStream) {
+ return new DeflaterOutputStream(outputStream, new Deflater(compressionLevel)) {
+ public void flush() throws IOException {
+ if (!supported) {
+ super.flush();
+ return;
+ }
+ int count = 0;
+ if (!def.needsInput()) {
+ do {
+ count = def.deflate(buf, 0, buf.length);
+ out.write(buf, 0, count);
+ } while (count > 0);
+ out.flush();
+ }
+ try {
+ do {
+ count = (Integer) method.invoke(def, buf, 0, buf.length, 2);
+ out.write(buf, 0, count);
+ } while (count > 0);
+ } catch (IllegalArgumentException e) {
+ throw new IOException("Can't flush");
+ } catch (IllegalAccessException e) {
+ throw new IOException("Can't flush");
+ } catch (InvocationTargetException e) {
+ throw new IOException("Can't flush");
+ }
+ super.flush();
+ }
+ };
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/compression/JzlibInputOutputStream.java b/src/org/jivesoftware/smack/compression/JzlibInputOutputStream.java
new file mode 100644
index 0000000..7db0773
--- /dev/null
+++ b/src/org/jivesoftware/smack/compression/JzlibInputOutputStream.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2013 Florian Schmaus
+ *
+ * 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.compression;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * This class provides XMPP "zlib" compression with the help of JZLib. Note that jzlib-1.0.7 must be used (i.e. in the
+ * classpath), newer versions won't work!
+ *
+ * @author Florian Schmaus
+ * @see <a href="http://www.jcraft.com/jzlib/">JZLib</a>
+ *
+ */
+public class JzlibInputOutputStream extends XMPPInputOutputStream {
+
+ private static Class<?> zoClass = null;
+ private static Class<?> ziClass = null;
+
+ static {
+ try {
+ zoClass = Class.forName("com.jcraft.jzlib.ZOutputStream");
+ ziClass = Class.forName("com.jcraft.jzlib.ZInputStream");
+ } catch (ClassNotFoundException e) {
+ }
+ }
+
+ public JzlibInputOutputStream() {
+ compressionMethod = "zlib";
+ }
+
+ @Override
+ public boolean isSupported() {
+ return (zoClass != null && ziClass != null);
+ }
+
+ @Override
+ public InputStream getInputStream(InputStream inputStream) throws SecurityException, NoSuchMethodException,
+ IllegalArgumentException, IllegalAccessException, InvocationTargetException, InstantiationException {
+ Constructor<?> constructor = ziClass.getConstructor(InputStream.class);
+ Object in = constructor.newInstance(inputStream);
+
+ Method method = ziClass.getMethod("setFlushMode", Integer.TYPE);
+ method.invoke(in, 2);
+ return (InputStream) in;
+ }
+
+ @Override
+ public OutputStream getOutputStream(OutputStream outputStream) throws SecurityException, NoSuchMethodException,
+ IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
+ Constructor<?> constructor = zoClass.getConstructor(OutputStream.class, Integer.TYPE);
+ Object out = constructor.newInstance(outputStream, 9);
+
+ Method method = zoClass.getMethod("setFlushMode", Integer.TYPE);
+ method.invoke(out, 2);
+ return (OutputStream) out;
+ }
+}
diff --git a/src/org/jivesoftware/smack/compression/XMPPInputOutputStream.java b/src/org/jivesoftware/smack/compression/XMPPInputOutputStream.java
new file mode 100644
index 0000000..d44416a
--- /dev/null
+++ b/src/org/jivesoftware/smack/compression/XMPPInputOutputStream.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2013 Florian Schmaus
+ *
+ * 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.compression;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public abstract class XMPPInputOutputStream {
+ protected String compressionMethod;
+
+ public String getCompressionMethod() {
+ return compressionMethod;
+ }
+
+ public abstract boolean isSupported();
+
+ public abstract InputStream getInputStream(InputStream inputStream) throws Exception;
+
+ public abstract OutputStream getOutputStream(OutputStream outputStream) throws Exception;
+}
diff --git a/src/org/jivesoftware/smack/debugger/ConsoleDebugger.java b/src/org/jivesoftware/smack/debugger/ConsoleDebugger.java
new file mode 100644
index 0000000..7e078b4
--- /dev/null
+++ b/src/org/jivesoftware/smack/debugger/ConsoleDebugger.java
@@ -0,0 +1,198 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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 org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.Connection;
+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 Connection connection = null;
+
+ private PacketListener listener = null;
+ private ConnectionListener connListener = null;
+
+ private Writer writer;
+ private Reader reader;
+ private ReaderListener readerListener;
+ private WriterListener writerListener;
+
+ public ConsoleDebugger(Connection 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 void reconnectionFailed(Exception e) {
+ System.out.println(
+ dateFormatter.format(new Date()) +
+ " Reconnection failed due to an exception (" +
+ connection.hashCode() +
+ ")");
+ e.printStackTrace();
+ }
+ public void reconnectionSuccessful() {
+ System.out.println(
+ dateFormatter.format(new Date()) + " Connection reconnected (" +
+ connection.hashCode() +
+ ")");
+ }
+ public void reconnectingIn(int seconds) {
+ System.out.println(
+ dateFormatter.format(new Date()) + " Connection (" +
+ connection.hashCode() +
+ ") will reconnect in " + seconds);
+ }
+ };
+ }
+
+ 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/src/org/jivesoftware/smack/debugger/SmackDebugger.java b/src/org/jivesoftware/smack/debugger/SmackDebugger.java
new file mode 100644
index 0000000..562720b
--- /dev/null
+++ b/src/org/jivesoftware/smack/debugger/SmackDebugger.java
@@ -0,0 +1,98 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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: Connection, 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/src/org/jivesoftware/smack/debugger/package.html b/src/org/jivesoftware/smack/debugger/package.html
new file mode 100644
index 0000000..afb861f
--- /dev/null
+++ b/src/org/jivesoftware/smack/debugger/package.html
@@ -0,0 +1 @@
+<body>Core debugger functionality.</body> \ No newline at end of file
diff --git a/src/org/jivesoftware/smack/filter/AndFilter.java b/src/org/jivesoftware/smack/filter/AndFilter.java
new file mode 100644
index 0000000..847b618
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/AndFilter.java
@@ -0,0 +1,91 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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 java.util.List;
+import java.util.ArrayList;
+
+/**
+ * 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 list of filters.
+ */
+ private List<PacketFilter> filters = new ArrayList<PacketFilter>();
+
+ /**
+ * Creates an empty AND filter. Filters should be added using the
+ * {@link #addFilter(PacketFilter)} method.
+ */
+ public AndFilter() {
+
+ }
+
+ /**
+ * Creates an AND filter using the specified filters.
+ *
+ * @param filters the filters to add.
+ */
+ public AndFilter(PacketFilter... filters) {
+ if (filters == null) {
+ throw new IllegalArgumentException("Parameter cannot be null.");
+ }
+ for(PacketFilter filter : filters) {
+ if(filter == null) {
+ throw new IllegalArgumentException("Parameter cannot be null.");
+ }
+ this.filters.add(filter);
+ }
+ }
+
+ /**
+ * 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.");
+ }
+ filters.add(filter);
+ }
+
+ public boolean accept(Packet packet) {
+ for (PacketFilter filter : filters) {
+ if (!filter.accept(packet)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public String toString() {
+ return filters.toString();
+ }
+}
diff --git a/src/org/jivesoftware/smack/filter/FromContainsFilter.java b/src/org/jivesoftware/smack/filter/FromContainsFilter.java
new file mode 100644
index 0000000..f8e9e97
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/FromContainsFilter.java
@@ -0,0 +1,54 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003 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/src/org/jivesoftware/smack/filter/FromMatchesFilter.java b/src/org/jivesoftware/smack/filter/FromMatchesFilter.java
new file mode 100644
index 0000000..e1dfa6c
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/FromMatchesFilter.java
@@ -0,0 +1,75 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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());
+ }
+ }
+
+ public String toString() {
+ return "FromMatchesFilter: " + address;
+ }
+}
diff --git a/src/org/jivesoftware/smack/filter/IQTypeFilter.java b/src/org/jivesoftware/smack/filter/IQTypeFilter.java
new file mode 100644
index 0000000..dbab1c3
--- /dev/null
+++ b/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/src/org/jivesoftware/smack/filter/MessageTypeFilter.java b/src/org/jivesoftware/smack/filter/MessageTypeFilter.java
new file mode 100644
index 0000000..a3430ec
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/MessageTypeFilter.java
@@ -0,0 +1,54 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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/src/org/jivesoftware/smack/filter/NotFilter.java b/src/org/jivesoftware/smack/filter/NotFilter.java
new file mode 100644
index 0000000..59537d0
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/NotFilter.java
@@ -0,0 +1,50 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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/src/org/jivesoftware/smack/filter/OrFilter.java b/src/org/jivesoftware/smack/filter/OrFilter.java
new file mode 100644
index 0000000..4c34fd0
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/OrFilter.java
@@ -0,0 +1,103 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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/src/org/jivesoftware/smack/filter/PacketExtensionFilter.java b/src/org/jivesoftware/smack/filter/PacketExtensionFilter.java
new file mode 100644
index 0000000..3cdc09c
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/PacketExtensionFilter.java
@@ -0,0 +1,61 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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;
+ }
+
+ /**
+ * Creates a new packet extension filter. Packets will pass the filter if they have a packet
+ * extension that matches the specified namespace.
+ *
+ * @param namespace the XML namespace of the packet extension.
+ */
+ public PacketExtensionFilter(String namespace) {
+ this(null, namespace);
+ }
+
+ public boolean accept(Packet packet) {
+ return packet.getExtension(elementName, namespace) != null;
+ }
+}
diff --git a/src/org/jivesoftware/smack/filter/PacketFilter.java b/src/org/jivesoftware/smack/filter/PacketFilter.java
new file mode 100644
index 0000000..634e68e
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/PacketFilter.java
@@ -0,0 +1,63 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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/src/org/jivesoftware/smack/filter/PacketIDFilter.java b/src/org/jivesoftware/smack/filter/PacketIDFilter.java
new file mode 100644
index 0000000..8d68201
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/PacketIDFilter.java
@@ -0,0 +1,53 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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());
+ }
+
+ public String toString() {
+ return "PacketIDFilter by id: " + packetID;
+ }
+}
diff --git a/src/org/jivesoftware/smack/filter/PacketTypeFilter.java b/src/org/jivesoftware/smack/filter/PacketTypeFilter.java
new file mode 100644
index 0000000..19c573f
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/PacketTypeFilter.java
@@ -0,0 +1,61 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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<? extends Packet> 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<? extends Packet> 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);
+ }
+
+ public String toString() {
+ return "PacketTypeFilter: " + packetType.getName();
+ }
+}
diff --git a/src/org/jivesoftware/smack/filter/ThreadFilter.java b/src/org/jivesoftware/smack/filter/ThreadFilter.java
new file mode 100644
index 0000000..8ba8b2e
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/ThreadFilter.java
@@ -0,0 +1,50 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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) {
+ return packet instanceof Message && thread.equals(((Message) packet).getThread());
+ }
+}
diff --git a/src/org/jivesoftware/smack/filter/ToContainsFilter.java b/src/org/jivesoftware/smack/filter/ToContainsFilter.java
new file mode 100644
index 0000000..8069fcc
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/ToContainsFilter.java
@@ -0,0 +1,55 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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/src/org/jivesoftware/smack/filter/package.html b/src/org/jivesoftware/smack/filter/package.html
new file mode 100644
index 0000000..8b3fe80
--- /dev/null
+++ b/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/src/org/jivesoftware/smack/package.html b/src/org/jivesoftware/smack/package.html
new file mode 100644
index 0000000..2758d78
--- /dev/null
+++ b/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/src/org/jivesoftware/smack/packet/Authentication.java b/src/org/jivesoftware/smack/packet/Authentication.java
new file mode 100644
index 0000000..a47c079
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Authentication.java
@@ -0,0 +1,186 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.Connection#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.Connection#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() {
+ StringBuilder buf = new StringBuilder();
+ 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/src/org/jivesoftware/smack/packet/Bind.java b/src/org/jivesoftware/smack/packet/Bind.java
new file mode 100644
index 0000000..07cd193
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Bind.java
@@ -0,0 +1,71 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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() {
+ StringBuilder buf = new StringBuilder();
+ 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/src/org/jivesoftware/smack/packet/DefaultPacketExtension.java b/src/org/jivesoftware/smack/packet/DefaultPacketExtension.java
new file mode 100644
index 0000000..6cc7934
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/DefaultPacketExtension.java
@@ -0,0 +1,133 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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>
+ * &lt;foo xmlns="http://bar.com"&gt;
+ * &lt;color&gt;blue&lt;/color&gt;
+ * &lt;food&gt;pizza&lt;/food&gt;
+ * &lt;/foo&gt;</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<String,String> 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() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\">");
+ for (String name : getNames()) {
+ 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 unmodifiable collection of the names that can be used to get
+ * values of the packet extension.
+ *
+ * @return the names.
+ */
+ public synchronized Collection<String> getNames() {
+ if (map == null) {
+ return Collections.emptySet();
+ }
+ return Collections.unmodifiableSet(new HashMap<String,String>(map).keySet());
+ }
+
+ /**
+ * 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 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<String,String>();
+ }
+ map.put(name, value);
+ }
+} \ No newline at end of file
diff --git a/src/org/jivesoftware/smack/packet/IQ.java b/src/org/jivesoftware/smack/packet/IQ.java
new file mode 100644
index 0000000..8e1f7d4
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/IQ.java
@@ -0,0 +1,244 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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>&lt;query xmlns="jabber:iq:auth"&gt; -- an authentication IQ.
+ * <li>&lt;query xmlns="jabber:iq:private"&gt; -- a private storage IQ.
+ * <li>&lt;pubsub xmlns="http://jabber.org/protocol/pubsub"&gt; -- a pubsub IQ.
+ * </ul>
+ *
+ * @author Matt Tucker
+ */
+public abstract class IQ extends Packet {
+
+ private Type type = Type.GET;
+
+ public IQ() {
+ super();
+ }
+
+ public IQ(IQ iq) {
+ super(iq);
+ type = iq.getType();
+ }
+ /**
+ * 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() {
+ StringBuilder buf = new StringBuilder();
+ 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();
+
+ /**
+ * Convenience method to create a new empty {@link Type#RESULT IQ.Type.RESULT}
+ * IQ based on a {@link Type#GET IQ.Type.GET} or {@link Type#SET IQ.Type.SET}
+ * IQ. The new packet will be initialized with:<ul>
+ * <li>The sender set to the recipient of the originating IQ.
+ * <li>The recipient set to the sender of the originating IQ.
+ * <li>The type set to {@link Type#RESULT IQ.Type.RESULT}.
+ * <li>The id set to the id of the originating IQ.
+ * <li>No child element of the IQ element.
+ * </ul>
+ *
+ * @param iq the {@link Type#GET IQ.Type.GET} or {@link Type#SET IQ.Type.SET} IQ packet.
+ * @throws IllegalArgumentException if the IQ packet does not have a type of
+ * {@link Type#GET IQ.Type.GET} or {@link Type#SET IQ.Type.SET}.
+ * @return a new {@link Type#RESULT IQ.Type.RESULT} IQ based on the originating IQ.
+ */
+ public static IQ createResultIQ(final IQ request) {
+ if (!(request.getType() == Type.GET || request.getType() == Type.SET)) {
+ throw new IllegalArgumentException(
+ "IQ must be of type 'set' or 'get'. Original IQ: " + request.toXML());
+ }
+ final IQ result = new IQ() {
+ public String getChildElementXML() {
+ return null;
+ }
+ };
+ result.setType(Type.RESULT);
+ result.setPacketID(request.getPacketID());
+ result.setFrom(request.getTo());
+ result.setTo(request.getFrom());
+ return result;
+ }
+
+ /**
+ * Convenience method to create a new {@link Type#ERROR IQ.Type.ERROR} IQ
+ * based on a {@link Type#GET IQ.Type.GET} or {@link Type#SET IQ.Type.SET}
+ * IQ. The new packet will be initialized with:<ul>
+ * <li>The sender set to the recipient of the originating IQ.
+ * <li>The recipient set to the sender of the originating IQ.
+ * <li>The type set to {@link Type#ERROR IQ.Type.ERROR}.
+ * <li>The id set to the id of the originating IQ.
+ * <li>The child element contained in the associated originating IQ.
+ * <li>The provided {@link XMPPError XMPPError}.
+ * </ul>
+ *
+ * @param iq the {@link Type#GET IQ.Type.GET} or {@link Type#SET IQ.Type.SET} IQ packet.
+ * @param error the error to associate with the created IQ packet.
+ * @throws IllegalArgumentException if the IQ packet does not have a type of
+ * {@link Type#GET IQ.Type.GET} or {@link Type#SET IQ.Type.SET}.
+ * @return a new {@link Type#ERROR IQ.Type.ERROR} IQ based on the originating IQ.
+ */
+ public static IQ createErrorResponse(final IQ request, final XMPPError error) {
+ if (!(request.getType() == Type.GET || request.getType() == Type.SET)) {
+ throw new IllegalArgumentException(
+ "IQ must be of type 'set' or 'get'. Original IQ: " + request.toXML());
+ }
+ final IQ result = new IQ() {
+ public String getChildElementXML() {
+ return request.getChildElementXML();
+ }
+ };
+ result.setType(Type.ERROR);
+ result.setPacketID(request.getPacketID());
+ result.setFrom(request.getTo());
+ result.setTo(request.getFrom());
+ result.setError(error);
+ return result;
+ }
+
+ /**
+ * 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/src/org/jivesoftware/smack/packet/Message.java b/src/org/jivesoftware/smack/packet/Message.java
new file mode 100644
index 0000000..d28a9f4
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Message.java
@@ -0,0 +1,672 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.*;
+
+/**
+ * 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>&nbsp;</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 thread = null;
+ private String language;
+
+ private final Set<Subject> subjects = new HashSet<Subject>();
+ private final Set<Body> bodies = new HashSet<Body>();
+
+ /**
+ * 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) {
+ 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) {
+ setTo(to);
+ this.type = type;
+ }
+
+ /**
+ * Returns the type of the message. If no type has been set this method will return {@link
+ * org.jivesoftware.smack.packet.Message.Type#normal}.
+ *
+ * @return the type of the message.
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Sets the type of the message.
+ *
+ * @param type the type of the message.
+ * @throws IllegalArgumentException if null is passed in as the type
+ */
+ public void setType(Type type) {
+ if (type == null) {
+ throw new IllegalArgumentException("Type cannot be null.");
+ }
+ this.type = type;
+ }
+
+ /**
+ * Returns the default subject of the message, or null if the subject has not been set.
+ * The subject is a short description of message contents.
+ * <p>
+ * The default subject of a message is the subject that corresponds to the message's language.
+ * (see {@link #getLanguage()}) or if no language is set to the applications default
+ * language (see {@link Packet#getDefaultLanguage()}).
+ *
+ * @return the subject of the message.
+ */
+ public String getSubject() {
+ return getSubject(null);
+ }
+
+ /**
+ * Returns the subject corresponding to the language. If the language is null, the method result
+ * will be the same as {@link #getSubject()}. Null will be returned if the language does not have
+ * a corresponding subject.
+ *
+ * @param language the language of the subject to return.
+ * @return the subject related to the passed in language.
+ */
+ public String getSubject(String language) {
+ Subject subject = getMessageSubject(language);
+ return subject == null ? null : subject.subject;
+ }
+
+ private Subject getMessageSubject(String language) {
+ language = determineLanguage(language);
+ for (Subject subject : subjects) {
+ if (language.equals(subject.language)) {
+ return subject;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a set of all subjects in this Message, including the default message subject accessible
+ * from {@link #getSubject()}.
+ *
+ * @return a collection of all subjects in this message.
+ */
+ public Collection<Subject> getSubjects() {
+ return Collections.unmodifiableCollection(subjects);
+ }
+
+ /**
+ * 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) {
+ if (subject == null) {
+ removeSubject(""); // use empty string because #removeSubject(null) is ambiguous
+ return;
+ }
+ addSubject(null, subject);
+ }
+
+ /**
+ * Adds a subject with a corresponding language.
+ *
+ * @param language the language of the subject being added.
+ * @param subject the subject being added to the message.
+ * @return the new {@link org.jivesoftware.smack.packet.Message.Subject}
+ * @throws NullPointerException if the subject is null, a null pointer exception is thrown
+ */
+ public Subject addSubject(String language, String subject) {
+ language = determineLanguage(language);
+ Subject messageSubject = new Subject(language, subject);
+ subjects.add(messageSubject);
+ return messageSubject;
+ }
+
+ /**
+ * Removes the subject with the given language from the message.
+ *
+ * @param language the language of the subject which is to be removed
+ * @return true if a subject was removed and false if it was not.
+ */
+ public boolean removeSubject(String language) {
+ language = determineLanguage(language);
+ for (Subject subject : subjects) {
+ if (language.equals(subject.language)) {
+ return subjects.remove(subject);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Removes the subject from the message and returns true if the subject was removed.
+ *
+ * @param subject the subject being removed from the message.
+ * @return true if the subject was successfully removed and false if it was not.
+ */
+ public boolean removeSubject(Subject subject) {
+ return subjects.remove(subject);
+ }
+
+ /**
+ * Returns all the languages being used for the subjects, not including the default subject.
+ *
+ * @return the languages being used for the subjects.
+ */
+ public Collection<String> getSubjectLanguages() {
+ Subject defaultSubject = getMessageSubject(null);
+ List<String> languages = new ArrayList<String>();
+ for (Subject subject : subjects) {
+ if (!subject.equals(defaultSubject)) {
+ languages.add(subject.language);
+ }
+ }
+ return Collections.unmodifiableCollection(languages);
+ }
+
+ /**
+ * Returns the default body of the message, or null if the body has not been set. The body
+ * is the main message contents.
+ * <p>
+ * The default body of a message is the body that corresponds to the message's language.
+ * (see {@link #getLanguage()}) or if no language is set to the applications default
+ * language (see {@link Packet#getDefaultLanguage()}).
+ *
+ * @return the body of the message.
+ */
+ public String getBody() {
+ return getBody(null);
+ }
+
+ /**
+ * Returns the body corresponding to the language. If the language is null, the method result
+ * will be the same as {@link #getBody()}. Null will be returned if the language does not have
+ * a corresponding body.
+ *
+ * @param language the language of the body to return.
+ * @return the body related to the passed in language.
+ * @since 3.0.2
+ */
+ public String getBody(String language) {
+ Body body = getMessageBody(language);
+ return body == null ? null : body.message;
+ }
+
+ private Body getMessageBody(String language) {
+ language = determineLanguage(language);
+ for (Body body : bodies) {
+ if (language.equals(body.language)) {
+ return body;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a set of all bodies in this Message, including the default message body accessible
+ * from {@link #getBody()}.
+ *
+ * @return a collection of all bodies in this Message.
+ * @since 3.0.2
+ */
+ public Collection<Body> getBodies() {
+ return Collections.unmodifiableCollection(bodies);
+ }
+
+ /**
+ * Sets the body of the message. The body is the main message contents.
+ *
+ * @param body the body of the message.
+ */
+ public void setBody(String body) {
+ if (body == null) {
+ removeBody(""); // use empty string because #removeBody(null) is ambiguous
+ return;
+ }
+ addBody(null, body);
+ }
+
+ /**
+ * Adds a body with a corresponding language.
+ *
+ * @param language the language of the body being added.
+ * @param body the body being added to the message.
+ * @return the new {@link org.jivesoftware.smack.packet.Message.Body}
+ * @throws NullPointerException if the body is null, a null pointer exception is thrown
+ * @since 3.0.2
+ */
+ public Body addBody(String language, String body) {
+ language = determineLanguage(language);
+ Body messageBody = new Body(language, body);
+ bodies.add(messageBody);
+ return messageBody;
+ }
+
+ /**
+ * Removes the body with the given language from the message.
+ *
+ * @param language the language of the body which is to be removed
+ * @return true if a body was removed and false if it was not.
+ */
+ public boolean removeBody(String language) {
+ language = determineLanguage(language);
+ for (Body body : bodies) {
+ if (language.equals(body.language)) {
+ return bodies.remove(body);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Removes the body from the message and returns true if the body was removed.
+ *
+ * @param body the body being removed from the message.
+ * @return true if the body was successfully removed and false if it was not.
+ * @since 3.0.2
+ */
+ public boolean removeBody(Body body) {
+ return bodies.remove(body);
+ }
+
+ /**
+ * Returns all the languages being used for the bodies, not including the default body.
+ *
+ * @return the languages being used for the bodies.
+ * @since 3.0.2
+ */
+ public Collection<String> getBodyLanguages() {
+ Body defaultBody = getMessageBody(null);
+ List<String> languages = new ArrayList<String>();
+ for (Body body : bodies) {
+ if (!body.equals(defaultBody)) {
+ languages.add(body.language);
+ }
+ }
+ return Collections.unmodifiableCollection(languages);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Returns the xml:lang of this Message.
+ *
+ * @return the xml:lang of this Message.
+ * @since 3.0.2
+ */
+ public String getLanguage() {
+ return language;
+ }
+
+ /**
+ * Sets the xml:lang of this Message.
+ *
+ * @param language the xml:lang of this Message.
+ * @since 3.0.2
+ */
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ private String determineLanguage(String language) {
+
+ // empty string is passed by #setSubject() and #setBody() and is the same as null
+ language = "".equals(language) ? null : language;
+
+ // if given language is null check if message language is set
+ if (language == null && this.language != null) {
+ return this.language;
+ }
+ else if (language == null) {
+ return getDefaultLanguage();
+ }
+ else {
+ return language;
+ }
+
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<message");
+ if (getXmlns() != null) {
+ buf.append(" xmlns=\"").append(getXmlns()).append("\"");
+ }
+ if (language != null) {
+ buf.append(" xml:lang=\"").append(getLanguage()).append("\"");
+ }
+ 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(">");
+ // Add the subject in the default language
+ Subject defaultSubject = getMessageSubject(null);
+ if (defaultSubject != null) {
+ buf.append("<subject>").append(StringUtils.escapeForXML(defaultSubject.subject)).append("</subject>");
+ }
+ // Add the subject in other languages
+ for (Subject subject : getSubjects()) {
+ // Skip the default language
+ if(subject.equals(defaultSubject))
+ continue;
+ buf.append("<subject xml:lang=\"").append(subject.language).append("\">");
+ buf.append(StringUtils.escapeForXML(subject.subject));
+ buf.append("</subject>");
+ }
+ // Add the body in the default language
+ Body defaultBody = getMessageBody(null);
+ if (defaultBody != null) {
+ buf.append("<body>").append(StringUtils.escapeForXML(defaultBody.message)).append("</body>");
+ }
+ // Add the bodies in other languages
+ for (Body body : getBodies()) {
+ // Skip the default language
+ if(body.equals(defaultBody))
+ continue;
+ buf.append("<body xml:lang=\"").append(body.getLanguage()).append("\">");
+ buf.append(StringUtils.escapeForXML(body.getMessage()));
+ buf.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();
+ }
+
+
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Message message = (Message) o;
+
+ if(!super.equals(message)) { return false; }
+ if (bodies.size() != message.bodies.size() || !bodies.containsAll(message.bodies)) {
+ return false;
+ }
+ if (language != null ? !language.equals(message.language) : message.language != null) {
+ return false;
+ }
+ if (subjects.size() != message.subjects.size() || !subjects.containsAll(message.subjects)) {
+ return false;
+ }
+ if (thread != null ? !thread.equals(message.thread) : message.thread != null) {
+ return false;
+ }
+ return type == message.type;
+
+ }
+
+ public int hashCode() {
+ int result;
+ result = (type != null ? type.hashCode() : 0);
+ result = 31 * result + subjects.hashCode();
+ result = 31 * result + (thread != null ? thread.hashCode() : 0);
+ result = 31 * result + (language != null ? language.hashCode() : 0);
+ result = 31 * result + bodies.hashCode();
+ return result;
+ }
+
+ /**
+ * Represents a message subject, its language and the content of the subject.
+ */
+ public static class Subject {
+
+ private String subject;
+ private String language;
+
+ private Subject(String language, String subject) {
+ if (language == null) {
+ throw new NullPointerException("Language cannot be null.");
+ }
+ if (subject == null) {
+ throw new NullPointerException("Subject cannot be null.");
+ }
+ this.language = language;
+ this.subject = subject;
+ }
+
+ /**
+ * Returns the language of this message subject.
+ *
+ * @return the language of this message subject.
+ */
+ public String getLanguage() {
+ return language;
+ }
+
+ /**
+ * Returns the subject content.
+ *
+ * @return the content of the subject.
+ */
+ public String getSubject() {
+ return subject;
+ }
+
+
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + this.language.hashCode();
+ result = prime * result + this.subject.hashCode();
+ return result;
+ }
+
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Subject other = (Subject) obj;
+ // simplified comparison because language and subject are always set
+ return this.language.equals(other.language) && this.subject.equals(other.subject);
+ }
+
+ }
+
+ /**
+ * Represents a message body, its language and the content of the message.
+ */
+ public static class Body {
+
+ private String message;
+ private String language;
+
+ private Body(String language, String message) {
+ if (language == null) {
+ throw new NullPointerException("Language cannot be null.");
+ }
+ if (message == null) {
+ throw new NullPointerException("Message cannot be null.");
+ }
+ this.language = language;
+ this.message = message;
+ }
+
+ /**
+ * Returns the language of this message body.
+ *
+ * @return the language of this message body.
+ */
+ public String getLanguage() {
+ return language;
+ }
+
+ /**
+ * Returns the message content.
+ *
+ * @return the content of the message.
+ */
+ public String getMessage() {
+ return message;
+ }
+
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + this.language.hashCode();
+ result = prime * result + this.message.hashCode();
+ return result;
+ }
+
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Body other = (Body) obj;
+ // simplified comparison because language and message are always set
+ return this.language.equals(other.language) && this.message.equals(other.message);
+ }
+
+ }
+
+ /**
+ * Represents the type of a message.
+ */
+ public enum Type {
+
+ /**
+ * (Default) a normal text message used in email like interface.
+ */
+ normal,
+
+ /**
+ * Typically short text message used in line-by-line chat interfaces.
+ */
+ chat,
+
+ /**
+ * Chat message sent to a groupchat server for group chats.
+ */
+ groupchat,
+
+ /**
+ * Text message to be displayed in scrolling marquee displays.
+ */
+ headline,
+
+ /**
+ * indicates a messaging error.
+ */
+ error;
+
+ public static Type fromString(String name) {
+ try {
+ return Type.valueOf(name);
+ }
+ catch (Exception e) {
+ return normal;
+ }
+ }
+
+ }
+}
diff --git a/src/org/jivesoftware/smack/packet/Packet.java b/src/org/jivesoftware/smack/packet/Packet.java
new file mode 100644
index 0000000..3f1185e
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Packet.java
@@ -0,0 +1,509 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.io.ByteArrayOutputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * 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 {
+
+ protected static final String DEFAULT_LANGUAGE =
+ java.util.Locale.getDefault().getLanguage().toLowerCase();
+
+ private static String DEFAULT_XML_NS = null;
+
+ /**
+ * 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";
+
+ /**
+ * Date format as defined in XEP-0082 - XMPP Date and Time Profiles.
+ * The time zone is set to UTC.
+ * <p>
+ * Date formats are not synchronized. Since multiple threads access the format concurrently,
+ * it must be synchronized externally.
+ */
+ public static final DateFormat XEP_0082_UTC_FORMAT = new SimpleDateFormat(
+ "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+ static {
+ XEP_0082_UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+
+ /**
+ * 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;
+
+ private String xmlns = DEFAULT_XML_NS;
+
+ /**
+ * Returns the next unique id. Each id made up of a short alphanumeric
+ * prefix along with a unique numeric value.
+ *
+ * @return the next id.
+ */
+ public static synchronized String nextID() {
+ return prefix + Long.toString(id++);
+ }
+
+ public static void setDefaultXmlns(String defaultXmlns) {
+ DEFAULT_XML_NS = defaultXmlns;
+ }
+
+ private String packetID = null;
+ private String to = null;
+ private String from = null;
+ private final List<PacketExtension> packetExtensions
+ = new CopyOnWriteArrayList<PacketExtension>();
+
+ private final Map<String,Object> properties = new HashMap<String, Object>();
+ private XMPPError error = null;
+
+ public Packet() {
+ }
+
+ public Packet(Packet p) {
+ packetID = p.getPacketID();
+ to = p.getTo();
+ from = p.getFrom();
+ xmlns = p.xmlns;
+ error = p.error;
+
+ // Copy extensions
+ for (PacketExtension pe : p.getExtensions()) {
+ addExtension(pe);
+ }
+ }
+
+ /**
+ * 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.<p>
+ *
+ * The StringUtils class provides several useful methods for dealing with
+ * XMPP addresses such as parsing the
+ * {@link StringUtils#parseBareAddress(String) bare address},
+ * {@link StringUtils#parseName(String) user name},
+ * {@link StringUtils#parseServer(String) server}, and
+ * {@link StringUtils#parseResource(String) resource}.
+ *
+ * @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.<p>
+ *
+ * The StringUtils class provides several useful methods for dealing with
+ * XMPP addresses such as parsing the
+ * {@link StringUtils#parseBareAddress(String) bare address},
+ * {@link StringUtils#parseName(String) user name},
+ * {@link StringUtils#parseServer(String) server}, and
+ * {@link StringUtils#parseResource(String) resource}.
+ *
+ * @return who the packet is being sent from, or <tt>null</tt> if the
+ * value 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 unmodifiable collection of the packet extensions attached to the packet.
+ *
+ * @return the packet extensions.
+ */
+ public synchronized Collection<PacketExtension> getExtensions() {
+ if (packetExtensions == null) {
+ return Collections.emptyList();
+ }
+ return Collections.unmodifiableList(new ArrayList<PacketExtension>(packetExtensions));
+ }
+
+ /**
+ * Returns the first extension of this packet that has the given namespace.
+ *
+ * @param namespace the namespace of the extension that is desired.
+ * @return the packet extension with the given namespace.
+ */
+ public PacketExtension getExtension(String namespace) {
+ return getExtension(null, namespace);
+ }
+
+ /**
+ * Returns the first packet extension that matches the specified element name and
+ * namespace, or <tt>null</tt> if it doesn't exist. If the provided elementName is null
+ * than only the provided namespace is attempted to be matched. 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. (May be null)
+ * @param namespace the XML element namespace of the packet extension.
+ * @return the extension, or <tt>null</tt> if it doesn't exist.
+ */
+ public PacketExtension getExtension(String elementName, String namespace) {
+ if (namespace == null) {
+ return null;
+ }
+ for (PacketExtension ext : packetExtensions) {
+ if ((elementName == null || elementName.equals(ext.getElementName()))
+ && namespace.equals(ext.getNamespace()))
+ {
+ return ext;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds a packet extension to the packet. Does nothing if extension is null.
+ *
+ * @param extension a packet extension.
+ */
+ public void addExtension(PacketExtension extension) {
+ if (extension == null) return;
+ packetExtensions.add(extension);
+ }
+
+ /**
+ * Adds a collection of packet extensions to the packet. Does nothing if extensions is null.
+ *
+ * @param extensions a collection of packet extensions
+ */
+ public void addExtensions(Collection<PacketExtension> extensions) {
+ if (extensions == null) return;
+ packetExtensions.addAll(extensions);
+ }
+
+ /**
+ * Removes a packet extension from the packet.
+ *
+ * @param extension the packet extension to remove.
+ */
+ public void removeExtension(PacketExtension extension) {
+ 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 originally 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 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");
+ }
+ 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 unmodifiable collection of all the property names that are set.
+ *
+ * @return all property names.
+ */
+ public synchronized Collection<String> getPropertyNames() {
+ if (properties == null) {
+ return Collections.emptySet();
+ }
+ return Collections.unmodifiableSet(new HashSet<String>(properties.keySet()));
+ }
+
+ /**
+ * 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() {
+ StringBuilder buf = new StringBuilder();
+ // Add in all standard extension sub-packets.
+ for (PacketExtension extension : getExtensions()) {
+ 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 (String name : getPropertyNames()) {
+ 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) {
+ // Ignore.
+ }
+ }
+ if (byteStream != null) {
+ try {
+ byteStream.close();
+ }
+ catch (Exception e) {
+ // Ignore.
+ }
+ }
+ }
+ }
+ buf.append("</property>");
+ }
+ buf.append("</properties>");
+ }
+ return buf.toString();
+ }
+
+ public String getXmlns() {
+ return this.xmlns;
+ }
+
+ /**
+ * Returns the default language used for all messages containing localized content.
+ *
+ * @return the default language
+ */
+ public static String getDefaultLanguage() {
+ return DEFAULT_LANGUAGE;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Packet packet = (Packet) o;
+
+ if (error != null ? !error.equals(packet.error) : packet.error != null) { return false; }
+ if (from != null ? !from.equals(packet.from) : packet.from != null) { return false; }
+ if (!packetExtensions.equals(packet.packetExtensions)) { return false; }
+ if (packetID != null ? !packetID.equals(packet.packetID) : packet.packetID != null) {
+ return false;
+ }
+ if (properties != null ? !properties.equals(packet.properties)
+ : packet.properties != null) {
+ return false;
+ }
+ if (to != null ? !to.equals(packet.to) : packet.to != null) { return false; }
+ return !(xmlns != null ? !xmlns.equals(packet.xmlns) : packet.xmlns != null);
+ }
+
+ public int hashCode() {
+ int result;
+ result = (xmlns != null ? xmlns.hashCode() : 0);
+ result = 31 * result + (packetID != null ? packetID.hashCode() : 0);
+ result = 31 * result + (to != null ? to.hashCode() : 0);
+ result = 31 * result + (from != null ? from.hashCode() : 0);
+ result = 31 * result + packetExtensions.hashCode();
+ result = 31 * result + properties.hashCode();
+ result = 31 * result + (error != null ? error.hashCode() : 0);
+ return result;
+ }
+}
diff --git a/src/org/jivesoftware/smack/packet/PacketExtension.java b/src/org/jivesoftware/smack/packet/PacketExtension.java
new file mode 100644
index 0000000..d2afbf8
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/PacketExtension.java
@@ -0,0 +1,56 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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 representation of the PacketExtension.
+ *
+ * @return the packet extension as XML.
+ */
+ public String toXML();
+}
diff --git a/src/org/jivesoftware/smack/packet/Presence.java b/src/org/jivesoftware/smack/packet/Presence.java
new file mode 100644
index 0000000..84fcfef
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Presence.java
@@ -0,0 +1,358 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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>{@link Presence.Type#available available} -- (Default) indicates the user is available to
+ * receive messages.
+ * <li>{@link Presence.Type#unavailable unavailable} -- the user is unavailable to receive messages.
+ * <li>{@link Presence.Type#subscribe subscribe} -- request subscription to recipient's presence.
+ * <li>{@link Presence.Type#subscribed subscribed} -- grant subscription to sender's presence.
+ * <li>{@link Presence.Type#unsubscribe unsubscribe} -- request removal of subscription to
+ * sender's presence.
+ * <li>{@link Presence.Type#unsubscribed unsubscribed} -- grant removal of subscription to
+ * sender's presence.
+ * <li>{@link Presence.Type#error error} -- 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: {@link Mode#available available} (the default),
+ * {@link Mode#chat chat}, {@link Mode#away away}, {@link Mode#xa xa} (extended away), and
+ * {@link Mode#dnd 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 = Integer.MIN_VALUE;
+ private Mode mode = null;
+ private String language;
+
+ /**
+ * Creates a new presence update. Status, priority, and mode are left un-set.
+ *
+ * @param type the type.
+ */
+ public Presence(Type type) {
+ setType(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) {
+ setType(type);
+ setStatus(status);
+ setPriority(priority);
+ setMode(mode);
+ }
+
+ /**
+ * Returns true if the {@link Type presence type} is available (online) and
+ * false if the user is unavailable (offline), or if this is a presence packet
+ * involved in a subscription operation. This is a convenience method
+ * equivalent to <tt>getType() == Presence.Type.available</tt>. Note that even
+ * when the user is available, their presence mode may be {@link Mode#away away},
+ * {@link Mode#xa extended away} or {@link Mode#dnd do not disturb}. Use
+ * {@link #isAway()} to determine if the user is away.
+ *
+ * @return true if the presence type is available.
+ */
+ public boolean isAvailable() {
+ return type == Type.available;
+ }
+
+ /**
+ * Returns true if the presence type is {@link Type#available available} and the presence
+ * mode is {@link Mode#away away}, {@link Mode#xa extended away}, or
+ * {@link Mode#dnd do not disturb}. False will be returned when the type or mode
+ * is any other value, including when the presence type is unavailable (offline).
+ * This is a convenience method equivalent to
+ * <tt>type == Type.available && (mode == Mode.away || mode == Mode.xa || mode == Mode.dnd)</tt>.
+ *
+ * @return true if the presence type is available and the presence mode is away, xa, or dnd.
+ */
+ public boolean isAway() {
+ return type == Type.available && (mode == Mode.away || mode == Mode.xa || mode == Mode.dnd);
+ }
+
+ /**
+ * 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) {
+ if(type == null) {
+ throw new NullPointerException("Type cannot be null");
+ }
+ 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 Integer.MIN_VALUE 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, or <tt>null</tt> if the mode is not set.
+ * A null presence mode value is interpreted to be the same thing as
+ * {@link Presence.Mode#available}.
+ *
+ * @return the mode.
+ */
+ public Mode getMode() {
+ return mode;
+ }
+
+ /**
+ * Sets the mode of the presence update. A null presence mode value is interpreted
+ * to be the same thing as {@link Presence.Mode#available}.
+ *
+ * @param mode the mode.
+ */
+ public void setMode(Mode mode) {
+ this.mode = mode;
+ }
+
+ /**
+ * Returns the xml:lang of this Presence, or null if one has not been set.
+ *
+ * @return the xml:lang of this Presence, or null if one has not been set.
+ * @since 3.0.2
+ */
+ public String getLanguage() {
+ return language;
+ }
+
+ /**
+ * Sets the xml:lang of this Presence.
+ *
+ * @param language the xml:lang of this Presence.
+ * @since 3.0.2
+ */
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<presence");
+ if(getXmlns() != null) {
+ buf.append(" xmlns=\"").append(getXmlns()).append("\"");
+ }
+ if (language != null) {
+ buf.append(" xml:lang=\"").append(getLanguage()).append("\"");
+ }
+ 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(StringUtils.escapeForXML(status)).append("</status>");
+ }
+ if (priority != Integer.MIN_VALUE) {
+ 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() {
+ StringBuilder buf = new StringBuilder();
+ buf.append(type);
+ if (mode != null) {
+ buf.append(": ").append(mode);
+ }
+ if (getStatus() != null) {
+ buf.append(" (").append(getStatus()).append(")");
+ }
+ return buf.toString();
+ }
+
+ /**
+ * A enum to represent the presecence type. Not that presence type is often confused
+ * with presence mode. Generally, if a user is signed into a server, they have a presence
+ * type of {@link #available available}, even if the mode is {@link Mode#away away},
+ * {@link Mode#dnd dnd}, etc. The presence type is only {@link #unavailable unavailable} when
+ * the user is signing out of the server.
+ */
+ public enum Type {
+
+ /**
+ * The user is available to receive messages (default).
+ */
+ available,
+
+ /**
+ * The user is unavailable to receive messages.
+ */
+ unavailable,
+
+ /**
+ * Request subscription to recipient's presence.
+ */
+ subscribe,
+
+ /**
+ * Grant subscription to sender's presence.
+ */
+ subscribed,
+
+ /**
+ * Request removal of subscription to sender's presence.
+ */
+ unsubscribe,
+
+ /**
+ * Grant removal of subscription to sender's presence.
+ */
+ unsubscribed,
+
+ /**
+ * The presence packet contains an error message.
+ */
+ error
+ }
+
+ /**
+ * An enum to represent the presence mode.
+ */
+ public enum Mode {
+
+ /**
+ * Free to chat.
+ */
+ chat,
+
+ /**
+ * Available (the default).
+ */
+ available,
+
+ /**
+ * Away.
+ */
+ away,
+
+ /**
+ * Away for an extended period of time.
+ */
+ xa,
+
+ /**
+ * Do not disturb.
+ */
+ dnd
+ }
+} \ No newline at end of file
diff --git a/src/org/jivesoftware/smack/packet/Privacy.java b/src/org/jivesoftware/smack/packet/Privacy.java
new file mode 100644
index 0000000..a62d578
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Privacy.java
@@ -0,0 +1,323 @@
+/**
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2006-2007 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.*;
+
+/**
+ * A Privacy IQ Packet, is used by the {@link org.jivesoftware.smack.PrivacyListManager}
+ * and {@link org.jivesoftware.smack.provider.PrivacyProvider} to allow and block
+ * communications from other users. It contains the appropriate structure to suit
+ * user-defined privacy lists. Different configured Privacy packages are used in the
+ * server & manager communication in order to:
+ * <ul>
+ * <li>Retrieving one's privacy lists.
+ * <li>Adding, removing, and editing one's privacy lists.
+ * <li>Setting, changing, or declining active lists.
+ * <li>Setting, changing, or declining the default list (i.e., the list that is active by default).
+ * </ul>
+ * Privacy Items can handle different kind of blocking communications based on JID, group,
+ * subscription type or globally {@link PrivacyItem}
+ *
+ * @author Francisco Vives
+ */
+public class Privacy extends IQ {
+ /** declineActiveList is true when the user declines the use of the active list **/
+ private boolean declineActiveList=false;
+ /** activeName is the name associated with the active list set for the session **/
+ private String activeName;
+ /** declineDefaultList is true when the user declines the use of the default list **/
+ private boolean declineDefaultList=false;
+ /** defaultName is the name of the default list that applies to the user as a whole **/
+ private String defaultName;
+ /** itemLists holds the set of privacy items classified in lists. It is a map where the
+ * key is the name of the list and the value a collection with privacy items. **/
+ private Map<String, List<PrivacyItem>> itemLists = new HashMap<String, List<PrivacyItem>>();
+
+ /**
+ * Set or update a privacy list with privacy items.
+ *
+ * @param listName the name of the new privacy list.
+ * @param listItem the {@link PrivacyItem} that rules the list.
+ * @return the privacy List.
+ */
+ public List<PrivacyItem> setPrivacyList(String listName, List<PrivacyItem> listItem) {
+ // Add new list to the itemLists
+ this.getItemLists().put(listName, listItem);
+ return listItem;
+ }
+
+ /**
+ * Set the active list based on the default list.
+ *
+ * @return the active List.
+ */
+ public List<PrivacyItem> setActivePrivacyList() {
+ this.setActiveName(this.getDefaultName());
+ return this.getItemLists().get(this.getActiveName());
+ }
+
+ /**
+ * Deletes an existing privacy list. If the privacy list being deleted was the default list
+ * then the user will end up with no default list. Therefore, the user will have to set a new
+ * default list.
+ *
+ * @param listName the name of the list being deleted.
+ */
+ public void deletePrivacyList(String listName) {
+ // Remove the list from the cache
+ this.getItemLists().remove(listName);
+
+ // Check if deleted list was the default list
+ if (this.getDefaultName() != null && listName.equals(this.getDefaultName())) {
+ this.setDefaultName(null);
+ }
+ }
+
+ /**
+ * Returns the active privacy list or <tt>null</tt> if none was found.
+ *
+ * @return list with {@link PrivacyItem} or <tt>null</tt> if none was found.
+ */
+ public List<PrivacyItem> getActivePrivacyList() {
+ // Check if we have the default list
+ if (this.getActiveName() == null) {
+ return null;
+ } else {
+ return this.getItemLists().get(this.getActiveName());
+ }
+ }
+
+ /**
+ * Returns the default privacy list or <tt>null</tt> if none was found.
+ *
+ * @return list with {@link PrivacyItem} or <tt>null</tt> if none was found.
+ */
+ public List<PrivacyItem> getDefaultPrivacyList() {
+ // Check if we have the default list
+ if (this.getDefaultName() == null) {
+ return null;
+ } else {
+ return this.getItemLists().get(this.getDefaultName());
+ }
+ }
+
+ /**
+ * Returns a specific privacy list.
+ *
+ * @param listName the name of the list to get.
+ * @return a List with {@link PrivacyItem}
+ */
+ public List<PrivacyItem> getPrivacyList(String listName) {
+ return this.getItemLists().get(listName);
+ }
+
+ /**
+ * Returns the privacy item in the specified order.
+ *
+ * @param listName the name of the privacy list.
+ * @param order the order of the element.
+ * @return a List with {@link PrivacyItem}
+ */
+ public PrivacyItem getItem(String listName, int order) {
+ Iterator<PrivacyItem> values = getPrivacyList(listName).iterator();
+ PrivacyItem itemFound = null;
+ while (itemFound == null && values.hasNext()) {
+ PrivacyItem element = values.next();
+ if (element.getOrder() == order) {
+ itemFound = element;
+ }
+ }
+ return itemFound;
+ }
+
+ /**
+ * Sets a given privacy list as the new user default list.
+ *
+ * @param newDefault the new default privacy list.
+ * @return if the default list was changed.
+ */
+ public boolean changeDefaultList(String newDefault) {
+ if (this.getItemLists().containsKey(newDefault)) {
+ this.setDefaultName(newDefault);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Remove the list.
+ *
+ * @param listName name of the list to remove.
+ */
+ public void deleteList(String listName) {
+ this.getItemLists().remove(listName);
+ }
+
+ /**
+ * Returns the name associated with the active list set for the session. Communications
+ * will be verified against the active list.
+ *
+ * @return the name of the active list.
+ */
+ public String getActiveName() {
+ return activeName;
+ }
+
+ /**
+ * Sets the name associated with the active list set for the session. Communications
+ * will be verified against the active list.
+ *
+ * @param activeName is the name of the active list.
+ */
+ public void setActiveName(String activeName) {
+ this.activeName = activeName;
+ }
+
+ /**
+ * Returns the name of the default list that applies to the user as a whole. Default list is
+ * processed if there is no active list set for the target session/resource to which a stanza
+ * is addressed, or if there are no current sessions for the user.
+ *
+ * @return the name of the default list.
+ */
+ public String getDefaultName() {
+ return defaultName;
+ }
+
+ /**
+ * Sets the name of the default list that applies to the user as a whole. Default list is
+ * processed if there is no active list set for the target session/resource to which a stanza
+ * is addressed, or if there are no current sessions for the user.
+ *
+ * If there is no default list set, then all Privacy Items are processed.
+ *
+ * @param defaultName is the name of the default list.
+ */
+ public void setDefaultName(String defaultName) {
+ this.defaultName = defaultName;
+ }
+
+ /**
+ * Returns the collection of privacy list that the user holds. A Privacy List contains a set of
+ * rules that define if communication with the list owner is allowed or denied.
+ * Users may have zero, one or more privacy items.
+ *
+ * @return a map where the key is the name of the list and the value the
+ * collection of privacy items.
+ */
+ public Map<String, List<PrivacyItem>> getItemLists() {
+ return itemLists;
+ }
+
+ /**
+ * Returns whether the receiver allows or declines the use of an active list.
+ *
+ * @return the decline status of the list.
+ */
+ public boolean isDeclineActiveList() {
+ return declineActiveList;
+ }
+
+ /**
+ * Sets whether the receiver allows or declines the use of an active list.
+ *
+ * @param declineActiveList indicates if the receiver declines the use of an active list.
+ */
+ public void setDeclineActiveList(boolean declineActiveList) {
+ this.declineActiveList = declineActiveList;
+ }
+
+ /**
+ * Returns whether the receiver allows or declines the use of a default list.
+ *
+ * @return the decline status of the list.
+ */
+ public boolean isDeclineDefaultList() {
+ return declineDefaultList;
+ }
+
+ /**
+ * Sets whether the receiver allows or declines the use of a default list.
+ *
+ * @param declineDefaultList indicates if the receiver declines the use of a default list.
+ */
+ public void setDeclineDefaultList(boolean declineDefaultList) {
+ this.declineDefaultList = declineDefaultList;
+ }
+
+ /**
+ * Returns all the list names the user has defined to group restrictions.
+ *
+ * @return a Set with Strings containing every list names.
+ */
+ public Set<String> getPrivacyListNames() {
+ return this.itemLists.keySet();
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<query xmlns=\"jabber:iq:privacy\">");
+
+ // Add the active tag
+ if (this.isDeclineActiveList()) {
+ buf.append("<active/>");
+ } else {
+ if (this.getActiveName() != null) {
+ buf.append("<active name=\"").append(this.getActiveName()).append("\"/>");
+ }
+ }
+ // Add the default tag
+ if (this.isDeclineDefaultList()) {
+ buf.append("<default/>");
+ } else {
+ if (this.getDefaultName() != null) {
+ buf.append("<default name=\"").append(this.getDefaultName()).append("\"/>");
+ }
+ }
+
+ // Add the list with their privacy items
+ for (Map.Entry<String, List<PrivacyItem>> entry : this.getItemLists().entrySet()) {
+ String listName = entry.getKey();
+ List<PrivacyItem> items = entry.getValue();
+ // Begin the list tag
+ if (items.isEmpty()) {
+ buf.append("<list name=\"").append(listName).append("\"/>");
+ } else {
+ buf.append("<list name=\"").append(listName).append("\">");
+ }
+ for (PrivacyItem item : items) {
+ // Append the item xml representation
+ buf.append(item.toXML());
+ }
+ // Close the list tag
+ if (!items.isEmpty()) {
+ buf.append("</list>");
+ }
+ }
+
+ // 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/src/org/jivesoftware/smack/packet/PrivacyItem.java b/src/org/jivesoftware/smack/packet/PrivacyItem.java
new file mode 100644
index 0000000..2e144ee
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/PrivacyItem.java
@@ -0,0 +1,462 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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;
+
+/**
+ * A privacy item acts a rule that when matched defines if a packet should be blocked or not.
+ *
+ * Privacy Items can handle different kind of blocking communications based on JID, group,
+ * subscription type or globally by:<ul>
+ * <li>Allowing or blocking messages.
+ * <li>Allowing or blocking inbound presence notifications.
+ * <li>Allowing or blocking outbound presence notifications.
+ * <li>Allowing or blocking IQ stanzas.
+ * <li>Allowing or blocking all communications.
+ * </ul>
+ * @author Francisco Vives
+ */
+public class PrivacyItem {
+ /** allow is the action associated with the item, it can allow or deny the communication. */
+ private boolean allow;
+ /** order is a non-negative integer that is unique among all items in the list. */
+ private int order;
+ /** rule hold the kind of communication ([jid|group|subscription]) it will allow or block and
+ * identifier to apply the action.
+ * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID.
+ * If the type is "group", then the 'value' attribute SHOULD contain the name of a group
+ * in the user's roster.
+ * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to",
+ * "from", or "none". */
+ private PrivacyRule rule;
+
+ /** blocks incoming IQ stanzas. */
+ private boolean filterIQ = false;
+ /** filterMessage blocks incoming message stanzas. */
+ private boolean filterMessage = false;
+ /** blocks incoming presence notifications. */
+ private boolean filterPresence_in = false;
+ /** blocks outgoing presence notifications. */
+ private boolean filterPresence_out = false;
+
+ /**
+ * Creates a new privacy item.
+ *
+ * @param type the type.
+ */
+ public PrivacyItem(String type, boolean allow, int order) {
+ this.setRule(PrivacyRule.fromString(type));
+ this.setAllow(allow);
+ this.setOrder(order);
+ }
+
+ /**
+ * Returns the action associated with the item, it MUST be filled and will allow or deny
+ * the communication.
+ *
+ * @return the allow communication status.
+ */
+ public boolean isAllow() {
+ return allow;
+ }
+
+ /**
+ * Sets the action associated with the item, it can allow or deny the communication.
+ *
+ * @param allow indicates if the receiver allow or deny the communication.
+ */
+ private void setAllow(boolean allow) {
+ this.allow = allow;
+ }
+
+
+ /**
+ * Returns whether the receiver allow or deny incoming IQ stanzas or not.
+ *
+ * @return the iq filtering status.
+ */
+ public boolean isFilterIQ() {
+ return filterIQ;
+ }
+
+
+ /**
+ * Sets whether the receiver allows or denies incoming IQ stanzas or not.
+ *
+ * @param filterIQ indicates if the receiver allows or denies incoming IQ stanzas.
+ */
+ public void setFilterIQ(boolean filterIQ) {
+ this.filterIQ = filterIQ;
+ }
+
+
+ /**
+ * Returns whether the receiver allows or denies incoming messages or not.
+ *
+ * @return the message filtering status.
+ */
+ public boolean isFilterMessage() {
+ return filterMessage;
+ }
+
+
+ /**
+ * Sets wheather the receiver allows or denies incoming messages or not.
+ *
+ * @param filterMessage indicates if the receiver allows or denies incoming messages or not.
+ */
+ public void setFilterMessage(boolean filterMessage) {
+ this.filterMessage = filterMessage;
+ }
+
+
+ /**
+ * Returns whether the receiver allows or denies incoming presence or not.
+ *
+ * @return the iq filtering incoming presence status.
+ */
+ public boolean isFilterPresence_in() {
+ return filterPresence_in;
+ }
+
+
+ /**
+ * Sets whether the receiver allows or denies incoming presence or not.
+ *
+ * @param filterPresence_in indicates if the receiver allows or denies filtering incoming presence.
+ */
+ public void setFilterPresence_in(boolean filterPresence_in) {
+ this.filterPresence_in = filterPresence_in;
+ }
+
+
+ /**
+ * Returns whether the receiver allows or denies incoming presence or not.
+ *
+ * @return the iq filtering incoming presence status.
+ */
+ public boolean isFilterPresence_out() {
+ return filterPresence_out;
+ }
+
+
+ /**
+ * Sets whether the receiver allows or denies outgoing presence or not.
+ *
+ * @param filterPresence_out indicates if the receiver allows or denies filtering outgoing presence
+ */
+ public void setFilterPresence_out(boolean filterPresence_out) {
+ this.filterPresence_out = filterPresence_out;
+ }
+
+
+ /**
+ * Returns the order where the receiver is processed. List items are processed in
+ * ascending order.
+ *
+ * The order MUST be filled and its value MUST be a non-negative integer
+ * that is unique among all items in the list.
+ *
+ * @return the order number.
+ */
+ public int getOrder() {
+ return order;
+ }
+
+
+ /**
+ * Sets the order where the receiver is processed.
+ *
+ * The order MUST be filled and its value MUST be a non-negative integer
+ * that is unique among all items in the list.
+ *
+ * @param order indicates the order in the list.
+ */
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ /**
+ * Sets the element identifier to apply the action.
+ *
+ * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID.
+ * If the type is "group", then the 'value' attribute SHOULD contain the name of a group
+ * in the user's roster.
+ * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to",
+ * "from", or "none".
+ *
+ * @param value is the identifier to apply the action.
+ */
+ public void setValue(String value) {
+ if (!(this.getRule() == null && value == null)) {
+ this.getRule().setValue(value);
+ }
+ }
+
+ /**
+ * Returns the type hold the kind of communication it will allow or block.
+ * It MUST be filled with one of these values: jid, group or subscription.
+ *
+ * @return the type of communication it represent.
+ */
+ public Type getType() {
+ if (this.getRule() == null) {
+ return null;
+ } else {
+ return this.getRule().getType();
+ }
+ }
+
+ /**
+ * Returns the element identifier to apply the action.
+ *
+ * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID.
+ * If the type is "group", then the 'value' attribute SHOULD contain the name of a group
+ * in the user's roster.
+ * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to",
+ * "from", or "none".
+ *
+ * @return the identifier to apply the action.
+ */
+ public String getValue() {
+ if (this.getRule() == null) {
+ return null;
+ } else {
+ return this.getRule().getValue();
+ }
+ }
+
+
+ /**
+ * Returns whether the receiver allows or denies every kind of communication.
+ *
+ * When filterIQ, filterMessage, filterPresence_in and filterPresence_out are not set
+ * the receiver will block all communications.
+ *
+ * @return the all communications status.
+ */
+ public boolean isFilterEverything() {
+ return !(this.isFilterIQ() || this.isFilterMessage() || this.isFilterPresence_in()
+ || this.isFilterPresence_out());
+ }
+
+
+ private PrivacyRule getRule() {
+ return rule;
+ }
+
+ private void setRule(PrivacyRule rule) {
+ this.rule = rule;
+ }
+ /**
+ * Answer an xml representation of the receiver according to the RFC 3921.
+ *
+ * @return the text xml representation.
+ */
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<item");
+ if (this.isAllow()) {
+ buf.append(" action=\"allow\"");
+ } else {
+ buf.append(" action=\"deny\"");
+ }
+ buf.append(" order=\"").append(getOrder()).append("\"");
+ if (getType() != null) {
+ buf.append(" type=\"").append(getType()).append("\"");
+ }
+ if (getValue() != null) {
+ buf.append(" value=\"").append(getValue()).append("\"");
+ }
+ if (isFilterEverything()) {
+ buf.append("/>");
+ } else {
+ buf.append(">");
+ if (this.isFilterIQ()) {
+ buf.append("<iq/>");
+ }
+ if (this.isFilterMessage()) {
+ buf.append("<message/>");
+ }
+ if (this.isFilterPresence_in()) {
+ buf.append("<presence-in/>");
+ }
+ if (this.isFilterPresence_out()) {
+ buf.append("<presence-out/>");
+ }
+ buf.append("</item>");
+ }
+ return buf.toString();
+ }
+
+
+ /**
+ * Privacy Rule represents the kind of action to apply.
+ * It holds the kind of communication ([jid|group|subscription]) it will allow or block and
+ * identifier to apply the action.
+ */
+
+ public static class PrivacyRule {
+ /**
+ * Type defines if the rule is based on JIDs, roster groups or presence subscription types.
+ * Available values are: [jid|group|subscription]
+ */
+ private Type type;
+ /**
+ * The value hold the element identifier to apply the action.
+ * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID.
+ * If the type is "group", then the 'value' attribute SHOULD contain the name of a group
+ * in the user's roster.
+ * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to",
+ * "from", or "none".
+ */
+ private String value;
+
+ /**
+ * If the type is "subscription", then the 'value' attribute MUST be one of "both",
+ * "to", "from", or "none"
+ */
+ public static final String SUBSCRIPTION_BOTH = "both";
+ public static final String SUBSCRIPTION_TO = "to";
+ public static final String SUBSCRIPTION_FROM = "from";
+ public static final String SUBSCRIPTION_NONE = "none";
+
+ /**
+ * Returns the type constant associated with the String value.
+ */
+ protected static PrivacyRule fromString(String value) {
+ if (value == null) {
+ return null;
+ }
+ PrivacyRule rule = new PrivacyRule();
+ rule.setType(Type.valueOf(value.toLowerCase()));
+ return rule;
+ }
+
+ /**
+ * Returns the type hold the kind of communication it will allow or block.
+ * It MUST be filled with one of these values: jid, group or subscription.
+ *
+ * @return the type of communication it represent.
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Sets the action associated with the item, it can allow or deny the communication.
+ *
+ * @param type indicates if the receiver allows or denies the communication.
+ */
+ private void setType(Type type) {
+ this.type = type;
+ }
+
+ /**
+ * Returns the element identifier to apply the action.
+ *
+ * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID.
+ * If the type is "group", then the 'value' attribute SHOULD contain the name of a group
+ * in the user's roster.
+ * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to",
+ * "from", or "none".
+ *
+ * @return the identifier to apply the action.
+ */
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Sets the element identifier to apply the action.
+ *
+ * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID.
+ * If the type is "group", then the 'value' attribute SHOULD contain the name of a group
+ * in the user's roster.
+ * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to",
+ * "from", or "none".
+ *
+ * @param value is the identifier to apply the action.
+ */
+ protected void setValue(String value) {
+ if (this.isSuscription()) {
+ setSuscriptionValue(value);
+ } else {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Sets the element identifier to apply the action.
+ *
+ * The 'value' attribute MUST be one of "both", "to", "from", or "none".
+ *
+ * @param value is the identifier to apply the action.
+ */
+ private void setSuscriptionValue(String value) {
+ String setValue;
+ if (value == null) {
+ // Do nothing
+ }
+ if (SUBSCRIPTION_BOTH.equalsIgnoreCase(value)) {
+ setValue = SUBSCRIPTION_BOTH;
+ }
+ else if (SUBSCRIPTION_TO.equalsIgnoreCase(value)) {
+ setValue = SUBSCRIPTION_TO;
+ }
+ else if (SUBSCRIPTION_FROM.equalsIgnoreCase(value)) {
+ setValue = SUBSCRIPTION_FROM;
+ }
+ else if (SUBSCRIPTION_NONE.equalsIgnoreCase(value)) {
+ setValue = SUBSCRIPTION_NONE;
+ }
+ // Default to available.
+ else {
+ setValue = null;
+ }
+ this.value = setValue;
+ }
+
+ /**
+ * Returns if the receiver represents a subscription rule.
+ *
+ * @return if the receiver represents a subscription rule.
+ */
+ public boolean isSuscription () {
+ return this.getType() == Type.subscription;
+ }
+ }
+
+ /**
+ * Type defines if the rule is based on JIDs, roster groups or presence subscription types.
+ */
+ public static enum Type {
+ /**
+ * JID being analyzed should belong to a roster group of the list's owner.
+ */
+ group,
+ /**
+ * JID being analyzed should have a resource match, domain match or bare JID match.
+ */
+ jid,
+ /**
+ * JID being analyzed should belong to a contact present in the owner's roster with
+ * the specified subscription status.
+ */
+ subscription
+ }
+}
diff --git a/src/org/jivesoftware/smack/packet/Registration.java b/src/org/jivesoftware/smack/packet/Registration.java
new file mode 100644
index 0000000..df22e27
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Registration.java
@@ -0,0 +1,155 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 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<String, String> attributes = new HashMap<String,String>();
+ private List<String> requiredFields = new ArrayList<String>();
+ private boolean registered = false;
+ private boolean remove = false;
+
+ /**
+ * 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<String, String> 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<String, String> attributes) {
+ this.attributes = attributes;
+ }
+
+ public List<String> getRequiredFields(){
+ return requiredFields;
+ }
+
+ public void addAttribute(String key, String value){
+ attributes.put(key, value);
+ }
+
+ public void setRegistered(boolean registered){
+ this.registered = registered;
+ }
+
+ public boolean isRegistered(){
+ return this.registered;
+ }
+
+ public String getField(String key){
+ return attributes.get(key);
+ }
+
+ public List<String> getFieldNames(){
+ return new ArrayList<String>(attributes.keySet());
+ }
+
+ public void setUsername(String username){
+ attributes.put("username", username);
+ }
+
+ public void setPassword(String password){
+ attributes.put("password", password);
+ }
+
+ public void setRemove(boolean remove){
+ this.remove = remove;
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<query xmlns=\"jabber:iq:register\">");
+ if (instructions != null && !remove) {
+ buf.append("<instructions>").append(instructions).append("</instructions>");
+ }
+ if (attributes != null && attributes.size() > 0 && !remove) {
+ for (String name : attributes.keySet()) {
+ String value = attributes.get(name);
+ buf.append("<").append(name).append(">");
+ buf.append(value);
+ buf.append("</").append(name).append(">");
+ }
+ }
+ else if(remove){
+ buf.append("</remove>");
+ }
+ // 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/src/org/jivesoftware/smack/packet/RosterPacket.java b/src/org/jivesoftware/smack/packet/RosterPacket.java
new file mode 100644
index 0000000..98483c8
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/RosterPacket.java
@@ -0,0 +1,311 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * Represents XMPP roster packets.
+ *
+ * @author Matt Tucker
+ */
+public class RosterPacket extends IQ {
+
+ private final List<Item> rosterItems = new ArrayList<Item>();
+ /*
+ * The ver attribute following XEP-0237
+ */
+ private String version;
+
+ /**
+ * Adds a roster item to the packet.
+ *
+ * @param item a roster item.
+ */
+ public void addRosterItem(Item item) {
+ synchronized (rosterItems) {
+ rosterItems.add(item);
+ }
+ }
+
+ public String getVersion(){
+ return version;
+ }
+
+ public void setVersion(String version){
+ this.version = version;
+ }
+
+ /**
+ * 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 unmodifiable collection for the roster items in the packet.
+ *
+ * @return an unmodifiable collection for the roster items in the packet.
+ */
+ public Collection<Item> getRosterItems() {
+ synchronized (rosterItems) {
+ return Collections.unmodifiableList(new ArrayList<Item>(rosterItems));
+ }
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<query xmlns=\"jabber:iq:roster\" ");
+ if(version!=null){
+ buf.append(" ver=\""+version+"\" ");
+ }
+ buf.append(">");
+ synchronized (rosterItems) {
+ for (Item entry : rosterItems) {
+ 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 final Set<String> 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 CopyOnWriteArraySet<String>();
+ }
+
+ /**
+ * 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 unmodifiable set of the group names that the roster item
+ * belongs to.
+ *
+ * @return an unmodifiable set of the group names.
+ */
+ public Set<String> getGroupNames() {
+ return Collections.unmodifiableSet(groupNames);
+ }
+
+ /**
+ * Adds a group name.
+ *
+ * @param groupName the group name.
+ */
+ public void addGroupName(String groupName) {
+ groupNames.add(groupName);
+ }
+
+ /**
+ * Removes a group name.
+ *
+ * @param groupName the group name.
+ */
+ public void removeGroupName(String groupName) {
+ groupNames.remove(groupName);
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<item jid=\"").append(user).append("\"");
+ if (name != null) {
+ buf.append(" name=\"").append(StringUtils.escapeForXML(name)).append("\"");
+ }
+ if (itemType != null) {
+ buf.append(" subscription=\"").append(itemType).append("\"");
+ }
+ if (itemStatus != null) {
+ buf.append(" ask=\"").append(itemStatus).append("\"");
+ }
+ buf.append(">");
+ for (String groupName : groupNames) {
+ buf.append("<group>").append(StringUtils.escapeForXML(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 UNSUBSCRIPTION_PENDING = new ItemStatus("unsubscribe");
+
+ public static ItemStatus fromString(String value) {
+ if (value == null) {
+ return null;
+ }
+ value = value.toLowerCase();
+ if ("unsubscribe".equals(value)) {
+ return UNSUBSCRIPTION_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;
+ }
+ }
+
+ public static enum ItemType {
+
+ /**
+ * The user and subscriber have no interest in each other's presence.
+ */
+ none,
+
+ /**
+ * The user is interested in receiving presence updates from the subscriber.
+ */
+ to,
+
+ /**
+ * The subscriber is interested in receiving presence updates from the user.
+ */
+ from,
+
+ /**
+ * The user and subscriber have a mutual interest in each other's presence.
+ */
+ both,
+
+ /**
+ * The user wishes to stop receiving presence updates from the subscriber.
+ */
+ remove
+ }
+}
diff --git a/src/org/jivesoftware/smack/packet/Session.java b/src/org/jivesoftware/smack/packet/Session.java
new file mode 100644
index 0000000..fd403ae
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Session.java
@@ -0,0 +1,45 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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/src/org/jivesoftware/smack/packet/StreamError.java b/src/org/jivesoftware/smack/packet/StreamError.java
new file mode 100644
index 0000000..8bb4c75
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/StreamError.java
@@ -0,0 +1,106 @@
+/**
+ * $Revision: 2408 $
+ * $Date: 2004-11-02 20:53:30 -0300 (Tue, 02 Nov 2004) $
+ *
+ * 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() {
+ StringBuilder txt = new StringBuilder();
+ txt.append("stream:error (").append(code).append(")");
+ return txt.toString();
+ }
+}
diff --git a/src/org/jivesoftware/smack/packet/XMPPError.java b/src/org/jivesoftware/smack/packet/XMPPError.java
new file mode 100644
index 0000000..770a09c
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/XMPPError.java
@@ -0,0 +1,453 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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 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, type,
+ * error condition as well as as an optional text explanation. Typical errors are:<p>
+ *
+ * <table border=1>
+ * <hr><td><b>Code</b></td><td><b>XMPP Error</b></td><td><b>Type</b></td></hr>
+ * <tr><td>500</td><td>interna-server-error</td><td>WAIT</td></tr>
+ * <tr><td>403</td><td>forbidden</td><td>AUTH</td></tr>
+ * <tr><td>400</td<td>bad-request</td><td>MODIFY</td>></tr>
+ * <tr><td>404</td><td>item-not-found</td><td>CANCEL</td></tr>
+ * <tr><td>409</td><td>conflict</td><td>CANCEL</td></tr>
+ * <tr><td>501</td><td>feature-not-implemented</td><td>CANCEL</td></tr>
+ * <tr><td>302</td><td>gone</td><td>MODIFY</td></tr>
+ * <tr><td>400</td><td>jid-malformed</td><td>MODIFY</td></tr>
+ * <tr><td>406</td><td>no-acceptable</td><td> MODIFY</td></tr>
+ * <tr><td>405</td><td>not-allowed</td><td>CANCEL</td></tr>
+ * <tr><td>401</td><td>not-authorized</td><td>AUTH</td></tr>
+ * <tr><td>402</td><td>payment-required</td><td>AUTH</td></tr>
+ * <tr><td>404</td><td>recipient-unavailable</td><td>WAIT</td></tr>
+ * <tr><td>302</td><td>redirect</td><td>MODIFY</td></tr>
+ * <tr><td>407</td><td>registration-required</td><td>AUTH</td></tr>
+ * <tr><td>404</td><td>remote-server-not-found</td><td>CANCEL</td></tr>
+ * <tr><td>504</td><td>remote-server-timeout</td><td>WAIT</td></tr>
+ * <tr><td>502</td><td>remote-server-error</td><td>CANCEL</td></tr>
+ * <tr><td>500</td><td>resource-constraint</td><td>WAIT</td></tr>
+ * <tr><td>503</td><td>service-unavailable</td><td>CANCEL</td></tr>
+ * <tr><td>407</td><td>subscription-required</td><td>AUTH</td></tr>
+ * <tr><td>500</td><td>undefined-condition</td><td>WAIT</td></tr>
+ * <tr><td>400</td><td>unexpected-condition</td><td>WAIT</td></tr>
+ * <tr><td>408</td><td>request-timeout</td><td>CANCEL</td></tr>
+ * </table>
+ *
+ * @author Matt Tucker
+ */
+public class XMPPError {
+
+ private int code;
+ private Type type;
+ private String condition;
+ private String message;
+ private List<PacketExtension> applicationExtensions = null;
+
+
+ /**
+ * Creates a new error with the specified condition infering the type and code.
+ * If the Condition is predefined, client code should be like:
+ * new XMPPError(XMPPError.Condition.remote_server_timeout);
+ * If the Condition is not predefined, invocations should be like
+ * new XMPPError(new XMPPError.Condition("my_own_error"));
+ *
+ * @param condition the error condition.
+ */
+ public XMPPError(Condition condition) {
+ this.init(condition);
+ this.message = null;
+ }
+
+ /**
+ * Creates a new error with the specified condition and message infering the type and code.
+ * If the Condition is predefined, client code should be like:
+ * new XMPPError(XMPPError.Condition.remote_server_timeout, "Error Explanation");
+ * If the Condition is not predefined, invocations should be like
+ * new XMPPError(new XMPPError.Condition("my_own_error"), "Error Explanation");
+ *
+ * @param condition the error condition.
+ * @param messageText a message describing the error.
+ */
+ public XMPPError(Condition condition, String messageText) {
+ this.init(condition);
+ this.message = messageText;
+ }
+
+ /**
+ * Creates a new error with the specified code and no message.
+ *
+ * @param code the error code.
+ * @deprecated new errors should be created using the constructor XMPPError(condition)
+ */
+ public XMPPError(int code) {
+ this.code = code;
+ this.message = null;
+ }
+
+ /**
+ * Creates a new error with the specified code and message.
+ * deprecated
+ *
+ * @param code the error code.
+ * @param message a message describing the error.
+ * @deprecated new errors should be created using the constructor XMPPError(condition, message)
+ */
+ public XMPPError(int code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+
+ /**
+ * Creates a new error with the specified code, type, condition and message.
+ * This constructor is used when the condition is not recognized automatically by XMPPError
+ * i.e. there is not a defined instance of ErrorCondition or it does not applies the default
+ * specification.
+ *
+ * @param code the error code.
+ * @param type the error type.
+ * @param condition the error condition.
+ * @param message a message describing the error.
+ * @param extension list of packet extensions
+ */
+ public XMPPError(int code, Type type, String condition, String message,
+ List<PacketExtension> extension) {
+ this.code = code;
+ this.type = type;
+ this.condition = condition;
+ this.message = message;
+ this.applicationExtensions = extension;
+ }
+
+ /**
+ * Initialize the error infering the type and code for the received condition.
+ *
+ * @param condition the error condition.
+ */
+ private void init(Condition condition) {
+ // Look for the condition and its default code and type
+ ErrorSpecification defaultErrorSpecification = ErrorSpecification.specFor(condition);
+ this.condition = condition.value;
+ if (defaultErrorSpecification != null) {
+ // If there is a default error specification for the received condition,
+ // it get configured with the infered type and code.
+ this.type = defaultErrorSpecification.getType();
+ this.code = defaultErrorSpecification.getCode();
+ }
+ }
+ /**
+ * Returns the error condition.
+ *
+ * @return the error condition.
+ */
+ public String getCondition() {
+ return condition;
+ }
+
+ /**
+ * Returns the error type.
+ *
+ * @return the error type.
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * 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() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<error code=\"").append(code).append("\"");
+ if (type != null) {
+ buf.append(" type=\"");
+ buf.append(type.name());
+ buf.append("\"");
+ }
+ buf.append(">");
+ if (condition != null) {
+ buf.append("<").append(condition);
+ buf.append(" xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\"/>");
+ }
+ if (message != null) {
+ buf.append("<text xml:lang=\"en\" xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\">");
+ buf.append(message);
+ buf.append("</text>");
+ }
+ for (PacketExtension element : this.getExtensions()) {
+ buf.append(element.toXML());
+ }
+ buf.append("</error>");
+ return buf.toString();
+ }
+
+ public String toString() {
+ StringBuilder txt = new StringBuilder();
+ if (condition != null) {
+ txt.append(condition);
+ }
+ txt.append("(").append(code).append(")");
+ if (message != null) {
+ txt.append(" ").append(message);
+ }
+ return txt.toString();
+ }
+
+ /**
+ * Returns an Iterator for the error extensions attached to the xmppError.
+ * An application MAY provide application-specific error information by including a
+ * properly-namespaced child in the error element.
+ *
+ * @return an Iterator for the error extensions.
+ */
+ public synchronized List<PacketExtension> getExtensions() {
+ if (applicationExtensions == null) {
+ return Collections.emptyList();
+ }
+ return Collections.unmodifiableList(applicationExtensions);
+ }
+
+ /**
+ * Returns the first patcket extension that matches the specified element name and
+ * namespace, or <tt>null</tt> if it doesn't exist.
+ *
+ * @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 (applicationExtensions == null || elementName == null || namespace == null) {
+ return null;
+ }
+ for (PacketExtension ext : applicationExtensions) {
+ if (elementName.equals(ext.getElementName()) && namespace.equals(ext.getNamespace())) {
+ return ext;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds a packet extension to the error.
+ *
+ * @param extension a packet extension.
+ */
+ public synchronized void addExtension(PacketExtension extension) {
+ if (applicationExtensions == null) {
+ applicationExtensions = new ArrayList<PacketExtension>();
+ }
+ applicationExtensions.add(extension);
+ }
+
+ /**
+ * Set the packet extension to the error.
+ *
+ * @param extension a packet extension.
+ */
+ public synchronized void setExtension(List<PacketExtension> extension) {
+ applicationExtensions = extension;
+ }
+
+ /**
+ * A class to represent the type of the Error. The types are:
+ *
+ * <ul>
+ * <li>XMPPError.Type.WAIT - retry after waiting (the error is temporary)
+ * <li>XMPPError.Type.CANCEL - do not retry (the error is unrecoverable)
+ * <li>XMPPError.Type.MODIFY - retry after changing the data sent
+ * <li>XMPPError.Type.AUTH - retry after providing credentials
+ * <li>XMPPError.Type.CONTINUE - proceed (the condition was only a warning)
+ * </ul>
+ */
+ public static enum Type {
+ WAIT,
+ CANCEL,
+ MODIFY,
+ AUTH,
+ CONTINUE
+ }
+
+ /**
+ * A class to represent predefined error conditions.
+ */
+ public static class Condition {
+
+ public static final Condition interna_server_error = new Condition("internal-server-error");
+ public static final Condition forbidden = new Condition("forbidden");
+ public static final Condition bad_request = new Condition("bad-request");
+ public static final Condition conflict = new Condition("conflict");
+ public static final Condition feature_not_implemented = new Condition("feature-not-implemented");
+ public static final Condition gone = new Condition("gone");
+ public static final Condition item_not_found = new Condition("item-not-found");
+ public static final Condition jid_malformed = new Condition("jid-malformed");
+ public static final Condition no_acceptable = new Condition("not-acceptable");
+ public static final Condition not_allowed = new Condition("not-allowed");
+ public static final Condition not_authorized = new Condition("not-authorized");
+ public static final Condition payment_required = new Condition("payment-required");
+ public static final Condition recipient_unavailable = new Condition("recipient-unavailable");
+ public static final Condition redirect = new Condition("redirect");
+ public static final Condition registration_required = new Condition("registration-required");
+ public static final Condition remote_server_error = new Condition("remote-server-error");
+ public static final Condition remote_server_not_found = new Condition("remote-server-not-found");
+ public static final Condition remote_server_timeout = new Condition("remote-server-timeout");
+ public static final Condition resource_constraint = new Condition("resource-constraint");
+ public static final Condition service_unavailable = new Condition("service-unavailable");
+ public static final Condition subscription_required = new Condition("subscription-required");
+ public static final Condition undefined_condition = new Condition("undefined-condition");
+ public static final Condition unexpected_request = new Condition("unexpected-request");
+ public static final Condition request_timeout = new Condition("request-timeout");
+
+ private String value;
+
+ public Condition(String value) {
+ this.value = value;
+ }
+
+ public String toString() {
+ return value;
+ }
+ }
+
+
+ /**
+ * A class to represent the error specification used to infer common usage.
+ */
+ private static class ErrorSpecification {
+ private int code;
+ private Type type;
+ private Condition condition;
+ private static Map<Condition, ErrorSpecification> instances = errorSpecifications();
+
+ private ErrorSpecification(Condition condition, Type type, int code) {
+ this.code = code;
+ this.type = type;
+ this.condition = condition;
+ }
+
+ private static Map<Condition, ErrorSpecification> errorSpecifications() {
+ Map<Condition, ErrorSpecification> instances = new HashMap<Condition, ErrorSpecification>(22);
+ instances.put(Condition.interna_server_error, new ErrorSpecification(
+ Condition.interna_server_error, Type.WAIT, 500));
+ instances.put(Condition.forbidden, new ErrorSpecification(Condition.forbidden,
+ Type.AUTH, 403));
+ instances.put(Condition.bad_request, new XMPPError.ErrorSpecification(
+ Condition.bad_request, Type.MODIFY, 400));
+ instances.put(Condition.item_not_found, new XMPPError.ErrorSpecification(
+ Condition.item_not_found, Type.CANCEL, 404));
+ instances.put(Condition.conflict, new XMPPError.ErrorSpecification(
+ Condition.conflict, Type.CANCEL, 409));
+ instances.put(Condition.feature_not_implemented, new XMPPError.ErrorSpecification(
+ Condition.feature_not_implemented, Type.CANCEL, 501));
+ instances.put(Condition.gone, new XMPPError.ErrorSpecification(
+ Condition.gone, Type.MODIFY, 302));
+ instances.put(Condition.jid_malformed, new XMPPError.ErrorSpecification(
+ Condition.jid_malformed, Type.MODIFY, 400));
+ instances.put(Condition.no_acceptable, new XMPPError.ErrorSpecification(
+ Condition.no_acceptable, Type.MODIFY, 406));
+ instances.put(Condition.not_allowed, new XMPPError.ErrorSpecification(
+ Condition.not_allowed, Type.CANCEL, 405));
+ instances.put(Condition.not_authorized, new XMPPError.ErrorSpecification(
+ Condition.not_authorized, Type.AUTH, 401));
+ instances.put(Condition.payment_required, new XMPPError.ErrorSpecification(
+ Condition.payment_required, Type.AUTH, 402));
+ instances.put(Condition.recipient_unavailable, new XMPPError.ErrorSpecification(
+ Condition.recipient_unavailable, Type.WAIT, 404));
+ instances.put(Condition.redirect, new XMPPError.ErrorSpecification(
+ Condition.redirect, Type.MODIFY, 302));
+ instances.put(Condition.registration_required, new XMPPError.ErrorSpecification(
+ Condition.registration_required, Type.AUTH, 407));
+ instances.put(Condition.remote_server_not_found, new XMPPError.ErrorSpecification(
+ Condition.remote_server_not_found, Type.CANCEL, 404));
+ instances.put(Condition.remote_server_timeout, new XMPPError.ErrorSpecification(
+ Condition.remote_server_timeout, Type.WAIT, 504));
+ instances.put(Condition.remote_server_error, new XMPPError.ErrorSpecification(
+ Condition.remote_server_error, Type.CANCEL, 502));
+ instances.put(Condition.resource_constraint, new XMPPError.ErrorSpecification(
+ Condition.resource_constraint, Type.WAIT, 500));
+ instances.put(Condition.service_unavailable, new XMPPError.ErrorSpecification(
+ Condition.service_unavailable, Type.CANCEL, 503));
+ instances.put(Condition.subscription_required, new XMPPError.ErrorSpecification(
+ Condition.subscription_required, Type.AUTH, 407));
+ instances.put(Condition.undefined_condition, new XMPPError.ErrorSpecification(
+ Condition.undefined_condition, Type.WAIT, 500));
+ instances.put(Condition.unexpected_request, new XMPPError.ErrorSpecification(
+ Condition.unexpected_request, Type.WAIT, 400));
+ instances.put(Condition.request_timeout, new XMPPError.ErrorSpecification(
+ Condition.request_timeout, Type.CANCEL, 408));
+
+ return instances;
+ }
+
+ protected static ErrorSpecification specFor(Condition condition) {
+ return instances.get(condition);
+ }
+
+ /**
+ * Returns the error condition.
+ *
+ * @return the error condition.
+ */
+ protected Condition getCondition() {
+ return condition;
+ }
+
+ /**
+ * Returns the error type.
+ *
+ * @return the error type.
+ */
+ protected Type getType() {
+ return type;
+ }
+
+ /**
+ * Returns the error code.
+ *
+ * @return the error code.
+ */
+ protected int getCode() {
+ return code;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/packet/package.html b/src/org/jivesoftware/smack/packet/package.html
new file mode 100644
index 0000000..18a6405
--- /dev/null
+++ b/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/src/org/jivesoftware/smack/provider/EmbeddedExtensionProvider.java b/src/org/jivesoftware/smack/provider/EmbeddedExtensionProvider.java
new file mode 100644
index 0000000..e7b4b93
--- /dev/null
+++ b/src/org/jivesoftware/smack/provider/EmbeddedExtensionProvider.java
@@ -0,0 +1,109 @@
+/**
+ * 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 java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.pubsub.provider.ItemProvider;
+import org.jivesoftware.smackx.pubsub.provider.ItemsProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * This class simplifies parsing of embedded elements by using the
+ * <a href="http://en.wikipedia.org/wiki/Template_method_pattern">Template Method Pattern</a>.
+ * After extracting the current element attributes and content of any child elements, the template method
+ * ({@link #createReturnExtension(String, String, Map, List)} is called. Subclasses
+ * then override this method to create the specific return type.
+ *
+ * <p>To use this class, you simply register your subclasses as extension providers in the
+ * <b>smack.properties</b> file. Then they will be automatically picked up and used to parse
+ * any child elements.
+ *
+ * <pre>
+ * For example, given the following message
+ *
+ * &lt;message from='pubsub.shakespeare.lit' to='francisco@denmark.lit' id='foo&gt;
+ * &lt;event xmlns='http://jabber.org/protocol/pubsub#event&gt;
+ * &lt;items node='princely_musings'&gt;
+ * &lt;item id='asdjkwei3i34234n356'&gt;
+ * &lt;entry xmlns='http://www.w3.org/2005/Atom'&gt;
+ * &lt;title&gt;Soliloquy&lt;/title&gt;
+ * &lt;link rel='alternative' type='text/html'/&gt;
+ * &lt;id>tag:denmark.lit,2003:entry-32397&lt;/id&gt;
+ * &lt;/entry&gt;
+ * &lt;/item&gt;
+ * &lt;/items&gt;
+ * &lt;/event&gt;
+ * &lt;/message&gt;
+ *
+ * I would have a classes
+ * {@link ItemsProvider} extends {@link EmbeddedExtensionProvider}
+ * {@link ItemProvider} extends {@link EmbeddedExtensionProvider}
+ * and
+ * AtomProvider extends {@link PacketExtensionProvider}
+ *
+ * These classes are then registered in the meta-inf/smack.providers file
+ * as follows.
+ *
+ * &lt;extensionProvider&gt;
+ * &lt;elementName&gt;items&lt;/elementName&gt;
+ * &lt;namespace&gt;http://jabber.org/protocol/pubsub#event&lt;/namespace&gt;
+ * &lt;className&gt;org.jivesoftware.smackx.provider.ItemsEventProvider&lt;/className&gt;
+ * &lt;/extensionProvider&gt;
+ * &lt;extensionProvider&gt;
+ * &lt;elementName&gt;item&lt;/elementName&gt;
+ * &lt;namespace&gt;http://jabber.org/protocol/pubsub#event&lt;/namespace&gt;
+ * &lt;className&gt;org.jivesoftware.smackx.provider.ItemProvider&lt;/className&gt;
+ * &lt;/extensionProvider&gt;
+ *
+ * </pre>
+ *
+ * @author Robin Collier
+ */
+abstract public class EmbeddedExtensionProvider implements PacketExtensionProvider
+{
+
+ final public PacketExtension parseExtension(XmlPullParser parser) throws Exception
+ {
+ String namespace = parser.getNamespace();
+ String name = parser.getName();
+ Map<String, String> attMap = new HashMap<String, String>();
+
+ for(int i=0; i<parser.getAttributeCount(); i++)
+ {
+ attMap.put(parser.getAttributeName(i), parser.getAttributeValue(i));
+ }
+ List<PacketExtension> extensions = new ArrayList<PacketExtension>();
+
+ do
+ {
+ int tag = parser.next();
+
+ if (tag == XmlPullParser.START_TAG)
+ extensions.add(PacketParserUtils.parsePacketExtension(parser.getName(), parser.getNamespace(), parser));
+ } while (!name.equals(parser.getName()));
+
+ return createReturnExtension(name, namespace, attMap, extensions);
+ }
+
+ abstract protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content);
+}
diff --git a/src/org/jivesoftware/smack/provider/IQProvider.java b/src/org/jivesoftware/smack/provider/IQProvider.java
new file mode 100644
index 0000000..936c6be
--- /dev/null
+++ b/src/org/jivesoftware/smack/provider/IQProvider.java
@@ -0,0 +1,47 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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/src/org/jivesoftware/smack/provider/PacketExtensionProvider.java b/src/org/jivesoftware/smack/provider/PacketExtensionProvider.java
new file mode 100644
index 0000000..8fc0af3
--- /dev/null
+++ b/src/org/jivesoftware/smack/provider/PacketExtensionProvider.java
@@ -0,0 +1,46 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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/src/org/jivesoftware/smack/provider/PrivacyProvider.java b/src/org/jivesoftware/smack/provider/PrivacyProvider.java
new file mode 100644
index 0000000..62b3120
--- /dev/null
+++ b/src/org/jivesoftware/smack/provider/PrivacyProvider.java
@@ -0,0 +1,151 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.DefaultPacketExtension;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Privacy;
+import org.jivesoftware.smack.packet.PrivacyItem;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.ArrayList;
+
+/**
+ * The PrivacyProvider parses {@link Privacy} packets. {@link Privacy}
+ * Parses the <tt>query</tt> sub-document and creates an instance of {@link Privacy}.
+ * For each <tt>item</tt> in the <tt>list</tt> element, it creates an instance
+ * of {@link PrivacyItem} and {@link org.jivesoftware.smack.packet.PrivacyItem.PrivacyRule}.
+ *
+ * @author Francisco Vives
+ */
+public class PrivacyProvider implements IQProvider {
+
+ public PrivacyProvider() {
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ Privacy privacy = new Privacy();
+ /* privacy.addExtension(PacketParserUtils.parsePacketExtension(parser
+ .getName(), parser.getNamespace(), parser)); */
+ privacy.addExtension(new DefaultPacketExtension(parser.getName(), parser.getNamespace()));
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("active")) {
+ String activeName = parser.getAttributeValue("", "name");
+ if (activeName == null) {
+ privacy.setDeclineActiveList(true);
+ } else {
+ privacy.setActiveName(activeName);
+ }
+ }
+ else if (parser.getName().equals("default")) {
+ String defaultName = parser.getAttributeValue("", "name");
+ if (defaultName == null) {
+ privacy.setDeclineDefaultList(true);
+ } else {
+ privacy.setDefaultName(defaultName);
+ }
+ }
+ else if (parser.getName().equals("list")) {
+ parseList(parser, privacy);
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+ }
+
+ return privacy;
+ }
+
+ // Parse the list complex type
+ public void parseList(XmlPullParser parser, Privacy privacy) throws Exception {
+ boolean done = false;
+ String listName = parser.getAttributeValue("", "name");
+ ArrayList<PrivacyItem> items = new ArrayList<PrivacyItem>();
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("item")) {
+ items.add(parseItem(parser));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("list")) {
+ done = true;
+ }
+ }
+ }
+
+ privacy.setPrivacyList(listName, items);
+ }
+
+ // Parse the list complex type
+ public PrivacyItem parseItem(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ // Retrieves the required attributes
+ String actionValue = parser.getAttributeValue("", "action");
+ String orderValue = parser.getAttributeValue("", "order");
+ String type = parser.getAttributeValue("", "type");
+
+ /*
+ * According the action value it sets the allow status. The fall-through action is assumed
+ * to be "allow"
+ */
+ boolean allow = true;
+ if ("allow".equalsIgnoreCase(actionValue)) {
+ allow = true;
+ } else if ("deny".equalsIgnoreCase(actionValue)) {
+ allow = false;
+ }
+ // Set the order number
+ int order = Integer.parseInt(orderValue);
+
+ // Create the privacy item
+ PrivacyItem item = new PrivacyItem(type, allow, order);
+ item.setValue(parser.getAttributeValue("", "value"));
+
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("iq")) {
+ item.setFilterIQ(true);
+ }
+ if (parser.getName().equals("message")) {
+ item.setFilterMessage(true);
+ }
+ if (parser.getName().equals("presence-in")) {
+ item.setFilterPresence_in(true);
+ }
+ if (parser.getName().equals("presence-out")) {
+ item.setFilterPresence_out(true);
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("item")) {
+ done = true;
+ }
+ }
+ }
+ return item;
+ }
+}
diff --git a/src/org/jivesoftware/smack/provider/ProviderManager.java b/src/org/jivesoftware/smack/provider/ProviderManager.java
new file mode 100644
index 0000000..4ddc8ad
--- /dev/null
+++ b/src/org/jivesoftware/smack/provider/ProviderManager.java
@@ -0,0 +1,438 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.XmlPullParserFactory;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 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>
+ * &lt;?xml version="1.0"?&gt;
+ * &lt;smackProviders&gt;
+ * &lt;iqProvider&gt;
+ * &lt;elementName&gt;query&lt;/elementName&gt;
+ * &lt;namespace&gt;jabber:iq:time&lt;/namespace&gt;
+ * &lt;className&gt;org.jivesoftware.smack.packet.Time&lt/className&gt;
+ * &lt;/iqProvider&gt;
+ * &lt;/smackProviders&gt;</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>
+ * &lt;iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'&gt;
+ * &lt;query xmlns='jabber:iq:time'&gt;
+ * &lt;utc&gt;20020910T17:58:35&lt;/utc&gt;
+ * &lt;tz&gt;MDT&lt;/tz&gt;
+ * &lt;display&gt;Tue Sep 10 12:58:35 2002&lt;/display&gt;
+ * &lt;/query&gt;
+ * &lt;/iq&gt;</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>
+ * &lt;?xml version="1.0"?&gt;
+ * &lt;smackProviders&gt;
+ * &lt;extensionProvider&gt;
+ * &lt;elementName&gt;x&lt;/elementName&gt;
+ * &lt;namespace&gt;jabber:iq:event&lt;/namespace&gt;
+ * &lt;className&gt;org.jivesoftware.smack.packet.MessageEvent&lt/className&gt;
+ * &lt;/extensionProvider&gt;
+ * &lt;/smackProviders&gt;</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.<p>
+ *
+ * It is possible to provide a custom provider manager instead of the default implementation
+ * provided by Smack. If you want to provide your own provider manager then you need to do it
+ * before creating any {@link org.jivesoftware.smack.Connection} by sending the static
+ * {@link #setInstance(ProviderManager)} message. Trying to change the provider manager after
+ * an Connection was created will result in an {@link IllegalStateException} error.
+ *
+ * @author Matt Tucker
+ */
+public class ProviderManager {
+
+ private static ProviderManager instance;
+
+ private Map<String, Object> extensionProviders = new ConcurrentHashMap<String, Object>();
+ private Map<String, Object> iqProviders = new ConcurrentHashMap<String, Object>();
+
+ /**
+ * Returns the only ProviderManager valid instance. Use {@link #setInstance(ProviderManager)}
+ * to configure your own provider manager. If non was provided then an instance of this
+ * class will be used.
+ *
+ * @return the only ProviderManager valid instance.
+ */
+ public static synchronized ProviderManager getInstance() {
+ if (instance == null) {
+ instance = new ProviderManager();
+ }
+ return instance;
+ }
+
+ /**
+ * Sets the only ProviderManager valid instance to be used by all Connections. If you
+ * want to provide your own provider manager then you need to do it before creating
+ * any Connection. Otherwise an IllegalStateException will be thrown.
+ *
+ * @param providerManager the only ProviderManager valid instance to be used.
+ * @throws IllegalStateException if a provider manager was already configued.
+ */
+ public static synchronized void setInstance(ProviderManager providerManager) {
+ if (instance != null) {
+ throw new IllegalStateException("ProviderManager singleton already set");
+ }
+ instance = providerManager;
+ }
+
+ 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 (ClassLoader classLoader : classLoaders) {
+ Enumeration<URL> providerEnum = classLoader.getResources(
+ "META-INF/smack.providers");
+ while (providerEnum.hasMoreElements()) {
+ URL url = providerEnum.nextElement();
+ InputStream providerStream = null;
+ try {
+ providerStream = url.openStream();
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ 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) {
+ // Ignore.
+ }
+ }
+ }
+ }
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 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>
+ * &lt;iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'&gt;
+ * &lt;query xmlns='jabber:iq:time'&gt;
+ * &lt;utc&gt;20020910T17:58:35&lt;/utc&gt;
+ * &lt;tz&gt;MDT&lt;/tz&gt;
+ * &lt;display&gt;Tue Sep 10 12:58:35 2002&lt;/display&gt;
+ * &lt;/query&gt;
+ * &lt;/iq&gt;</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 unmodifiable collection of all IQProvider instances. Each object
+ * in the collection will either be an IQProvider instance, or a Class object
+ * that implements the IQProvider interface.
+ *
+ * @return all IQProvider instances.
+ */
+ public Collection<Object> getIQProviders() {
+ return Collections.unmodifiableCollection(iqProviders.values());
+ }
+
+ /**
+ * 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 an IQ provider with the specified element name and namespace. This
+ * method is typically called to cleanup providers that are programatically added
+ * using the {@link #addIQProvider(String, String, Object) addIQProvider} method.
+ *
+ * @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>
+ * &lt;message to='romeo@montague.net' id='message_1'&gt;
+ * &lt;body&gt;Art thou not Romeo, and a Montague?&lt;/body&gt;
+ * &lt;x xmlns='jabber:x:event'&gt;
+ * &lt;composing/&gt;
+ * &lt;/x&gt;
+ * &lt;/message&gt;</pre>
+ *
+ * <p>Note: this method is generally only called by the internal Smack classes.
+ *
+ * @param elementName element name associated with extension provider.
+ * @param namespace namespace associated with extension provider.
+ * @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 an extension provider with the specified element name and namespace. This
+ * method is typically called to cleanup providers that are programatically added
+ * using the {@link #addExtensionProvider(String, String, Object) addExtensionProvider} method.
+ *
+ * @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 unmodifiable collection of all PacketExtensionProvider instances. Each object
+ * in the collection will either be a PacketExtensionProvider instance, or a Class object
+ * that implements the PacketExtensionProvider interface.
+ *
+ * @return all PacketExtensionProvider instances.
+ */
+ public Collection<Object> getExtensionProviders() {
+ return Collections.unmodifiableCollection(extensionProviders.values());
+ }
+
+ /**
+ * 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 String getProviderKey(String elementName, String namespace) {
+ StringBuilder buf = new StringBuilder();
+ 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 ClassLoader[] getClassLoaders() {
+ ClassLoader[] classLoaders = new ClassLoader[2];
+ classLoaders[0] = ProviderManager.class.getClassLoader();
+ classLoaders[1] = Thread.currentThread().getContextClassLoader();
+ // Clean up possible null values. Note that #getClassLoader may return a null value.
+ List<ClassLoader> loaders = new ArrayList<ClassLoader>();
+ for (ClassLoader classLoader : classLoaders) {
+ if (classLoader != null) {
+ loaders.add(classLoader);
+ }
+ }
+ return loaders.toArray(new ClassLoader[loaders.size()]);
+ }
+
+ private ProviderManager() {
+ super();
+ initialize();
+ }
+} \ No newline at end of file
diff --git a/src/org/jivesoftware/smack/provider/package.html b/src/org/jivesoftware/smack/provider/package.html
new file mode 100644
index 0000000..fccc383
--- /dev/null
+++ b/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/src/org/jivesoftware/smack/proxy/DirectSocketFactory.java b/src/org/jivesoftware/smack/proxy/DirectSocketFactory.java
new file mode 100644
index 0000000..6197c38
--- /dev/null
+++ b/src/org/jivesoftware/smack/proxy/DirectSocketFactory.java
@@ -0,0 +1,74 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.proxy;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import javax.net.SocketFactory;
+
+/**
+ * SocketFactory for direct connection
+ *
+ * @author Atul Aggarwal
+ */
+class DirectSocketFactory
+ extends SocketFactory
+{
+
+ public DirectSocketFactory()
+ {
+ }
+
+ static private int roundrobin = 0;
+
+ public Socket createSocket(String host, int port)
+ throws IOException, UnknownHostException
+ {
+ Socket newSocket = new Socket(Proxy.NO_PROXY);
+ InetAddress resolved[] = InetAddress.getAllByName(host);
+ newSocket.connect(new InetSocketAddress(resolved[(roundrobin++) % resolved.length],port));
+ return newSocket;
+ }
+
+ public Socket createSocket(String host ,int port, InetAddress localHost,
+ int localPort)
+ throws IOException, UnknownHostException
+ {
+ return new Socket(host,port,localHost,localPort);
+ }
+
+ public Socket createSocket(InetAddress host, int port)
+ throws IOException
+ {
+ Socket newSocket = new Socket(Proxy.NO_PROXY);
+ newSocket.connect(new InetSocketAddress(host,port));
+ return newSocket;
+ }
+
+ public Socket createSocket( InetAddress address, int port,
+ InetAddress localAddress, int localPort)
+ throws IOException
+ {
+ return new Socket(address,port,localAddress,localPort);
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/proxy/HTTPProxySocketFactory.java b/src/org/jivesoftware/smack/proxy/HTTPProxySocketFactory.java
new file mode 100644
index 0000000..4ee5dd6
--- /dev/null
+++ b/src/org/jivesoftware/smack/proxy/HTTPProxySocketFactory.java
@@ -0,0 +1,172 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.proxy;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import javax.net.SocketFactory;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Http Proxy Socket Factory which returns socket connected to Http Proxy
+ *
+ * @author Atul Aggarwal
+ */
+class HTTPProxySocketFactory
+ extends SocketFactory
+{
+
+ private ProxyInfo proxy;
+
+ public HTTPProxySocketFactory(ProxyInfo proxy)
+ {
+ this.proxy = proxy;
+ }
+
+ public Socket createSocket(String host, int port)
+ throws IOException, UnknownHostException
+ {
+ return httpProxifiedSocket(host, port);
+ }
+
+ public Socket createSocket(String host ,int port, InetAddress localHost,
+ int localPort)
+ throws IOException, UnknownHostException
+ {
+ return httpProxifiedSocket(host, port);
+ }
+
+ public Socket createSocket(InetAddress host, int port)
+ throws IOException
+ {
+ return httpProxifiedSocket(host.getHostAddress(), port);
+
+ }
+
+ public Socket createSocket( InetAddress address, int port,
+ InetAddress localAddress, int localPort)
+ throws IOException
+ {
+ return httpProxifiedSocket(address.getHostAddress(), port);
+ }
+
+ private Socket httpProxifiedSocket(String host, int port)
+ throws IOException
+ {
+ String proxyhost = proxy.getProxyAddress();
+ int proxyPort = proxy.getProxyPort();
+ Socket socket = new Socket(proxyhost,proxyPort);
+ String hostport = "CONNECT " + host + ":" + port;
+ String proxyLine;
+ String username = proxy.getProxyUsername();
+ if (username == null)
+ {
+ proxyLine = "";
+ }
+ else
+ {
+ String password = proxy.getProxyPassword();
+ proxyLine = "\r\nProxy-Authorization: Basic " + StringUtils.encodeBase64(username + ":" + password);
+ }
+ socket.getOutputStream().write((hostport + " HTTP/1.1\r\nHost: "
+ + hostport + proxyLine + "\r\n\r\n").getBytes("UTF-8"));
+
+ InputStream in = socket.getInputStream();
+ StringBuilder got = new StringBuilder(100);
+ int nlchars = 0;
+
+ while (true)
+ {
+ char c = (char) in.read();
+ got.append(c);
+ if (got.length() > 1024)
+ {
+ throw new ProxyException(ProxyInfo.ProxyType.HTTP, "Recieved " +
+ "header of >1024 characters from "
+ + proxyhost + ", cancelling connection");
+ }
+ if (c == -1)
+ {
+ throw new ProxyException(ProxyInfo.ProxyType.HTTP);
+ }
+ if ((nlchars == 0 || nlchars == 2) && c == '\r')
+ {
+ nlchars++;
+ }
+ else if ((nlchars == 1 || nlchars == 3) && c == '\n')
+ {
+ nlchars++;
+ }
+ else
+ {
+ nlchars = 0;
+ }
+ if (nlchars == 4)
+ {
+ break;
+ }
+ }
+
+ if (nlchars != 4)
+ {
+ throw new ProxyException(ProxyInfo.ProxyType.HTTP, "Never " +
+ "received blank line from "
+ + proxyhost + ", cancelling connection");
+ }
+
+ String gotstr = got.toString();
+
+ BufferedReader br = new BufferedReader(new StringReader(gotstr));
+ String response = br.readLine();
+
+ if (response == null)
+ {
+ throw new ProxyException(ProxyInfo.ProxyType.HTTP, "Empty proxy " +
+ "response from " + proxyhost + ", cancelling");
+ }
+
+ Matcher m = RESPONSE_PATTERN.matcher(response);
+ if (!m.matches())
+ {
+ throw new ProxyException(ProxyInfo.ProxyType.HTTP , "Unexpected " +
+ "proxy response from " + proxyhost + ": " + response);
+ }
+
+ int code = Integer.parseInt(m.group(1));
+
+ if (code != HttpURLConnection.HTTP_OK)
+ {
+ throw new ProxyException(ProxyInfo.ProxyType.HTTP);
+ }
+
+ return socket;
+ }
+
+ private static final Pattern RESPONSE_PATTERN
+ = Pattern.compile("HTTP/\\S+\\s(\\d+)\\s(.*)\\s*");
+
+}
diff --git a/src/org/jivesoftware/smack/proxy/ProxyException.java b/src/org/jivesoftware/smack/proxy/ProxyException.java
new file mode 100644
index 0000000..b37910c
--- /dev/null
+++ b/src/org/jivesoftware/smack/proxy/ProxyException.java
@@ -0,0 +1,44 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.proxy;
+
+import java.io.IOException;
+
+/**
+ * An exception class to handle exceptions caused by proxy.
+ *
+ * @author Atul Aggarwal
+ */
+public class ProxyException
+ extends IOException
+{
+ public ProxyException(ProxyInfo.ProxyType type, String ex, Throwable cause)
+ {
+ super("Proxy Exception " + type.toString() + " : "+ex+", "+cause);
+ }
+
+ public ProxyException(ProxyInfo.ProxyType type, String ex)
+ {
+ super("Proxy Exception " + type.toString() + " : "+ex);
+ }
+
+ public ProxyException(ProxyInfo.ProxyType type)
+ {
+ super("Proxy Exception " + type.toString() + " : " + "Unknown Error");
+ }
+}
diff --git a/src/org/jivesoftware/smack/proxy/ProxyInfo.java b/src/org/jivesoftware/smack/proxy/ProxyInfo.java
new file mode 100644
index 0000000..5a7d354
--- /dev/null
+++ b/src/org/jivesoftware/smack/proxy/ProxyInfo.java
@@ -0,0 +1,131 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.proxy;
+
+import javax.net.SocketFactory;
+
+/**
+ * Class which stores proxy information such as proxy type, host, port,
+ * authentication etc.
+ *
+ * @author Atul Aggarwal
+ */
+
+public class ProxyInfo
+{
+ public static enum ProxyType
+ {
+ NONE,
+ HTTP,
+ SOCKS4,
+ SOCKS5
+ }
+
+ private String proxyAddress;
+ private int proxyPort;
+ private String proxyUsername;
+ private String proxyPassword;
+ private ProxyType proxyType;
+
+ public ProxyInfo( ProxyType pType, String pHost, int pPort, String pUser,
+ String pPass)
+ {
+ this.proxyType = pType;
+ this.proxyAddress = pHost;
+ this.proxyPort = pPort;
+ this.proxyUsername = pUser;
+ this.proxyPassword = pPass;
+ }
+
+ public static ProxyInfo forHttpProxy(String pHost, int pPort, String pUser,
+ String pPass)
+ {
+ return new ProxyInfo(ProxyType.HTTP, pHost, pPort, pUser, pPass);
+ }
+
+ public static ProxyInfo forSocks4Proxy(String pHost, int pPort, String pUser,
+ String pPass)
+ {
+ return new ProxyInfo(ProxyType.SOCKS4, pHost, pPort, pUser, pPass);
+ }
+
+ public static ProxyInfo forSocks5Proxy(String pHost, int pPort, String pUser,
+ String pPass)
+ {
+ return new ProxyInfo(ProxyType.SOCKS5, pHost, pPort, pUser, pPass);
+ }
+
+ public static ProxyInfo forNoProxy()
+ {
+ return new ProxyInfo(ProxyType.NONE, null, 0, null, null);
+ }
+
+ public static ProxyInfo forDefaultProxy()
+ {
+ return new ProxyInfo(ProxyType.NONE, null, 0, null, null);
+ }
+
+ public ProxyType getProxyType()
+ {
+ return proxyType;
+ }
+
+ public String getProxyAddress()
+ {
+ return proxyAddress;
+ }
+
+ public int getProxyPort()
+ {
+ return proxyPort;
+ }
+
+ public String getProxyUsername()
+ {
+ return proxyUsername;
+ }
+
+ public String getProxyPassword()
+ {
+ return proxyPassword;
+ }
+
+ public SocketFactory getSocketFactory()
+ {
+ if(proxyType == ProxyType.NONE)
+ {
+ return new DirectSocketFactory();
+ }
+ else if(proxyType == ProxyType.HTTP)
+ {
+ return new HTTPProxySocketFactory(this);
+ }
+ else if(proxyType == ProxyType.SOCKS4)
+ {
+ return new Socks4ProxySocketFactory(this);
+ }
+ else if(proxyType == ProxyType.SOCKS5)
+ {
+ return new Socks5ProxySocketFactory(this);
+ }
+ else
+ {
+ return null;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/proxy/Socks4ProxySocketFactory.java b/src/org/jivesoftware/smack/proxy/Socks4ProxySocketFactory.java
new file mode 100644
index 0000000..6a32c11
--- /dev/null
+++ b/src/org/jivesoftware/smack/proxy/Socks4ProxySocketFactory.java
@@ -0,0 +1,216 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.proxy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import javax.net.SocketFactory;
+
+/**
+ * Socket factory for socks4 proxy
+ *
+ * @author Atul Aggarwal
+ */
+public class Socks4ProxySocketFactory
+ extends SocketFactory
+{
+ private ProxyInfo proxy;
+
+ public Socks4ProxySocketFactory(ProxyInfo proxy)
+ {
+ this.proxy = proxy;
+ }
+
+ public Socket createSocket(String host, int port)
+ throws IOException, UnknownHostException
+ {
+ return socks4ProxifiedSocket(host,port);
+
+ }
+
+ public Socket createSocket(String host ,int port, InetAddress localHost,
+ int localPort)
+ throws IOException, UnknownHostException
+ {
+ return socks4ProxifiedSocket(host,port);
+ }
+
+ public Socket createSocket(InetAddress host, int port)
+ throws IOException
+ {
+ return socks4ProxifiedSocket(host.getHostAddress(),port);
+ }
+
+ public Socket createSocket( InetAddress address, int port,
+ InetAddress localAddress, int localPort)
+ throws IOException
+ {
+ return socks4ProxifiedSocket(address.getHostAddress(),port);
+
+ }
+
+ private Socket socks4ProxifiedSocket(String host, int port)
+ throws IOException
+ {
+ Socket socket = null;
+ InputStream in = null;
+ OutputStream out = null;
+ String proxy_host = proxy.getProxyAddress();
+ int proxy_port = proxy.getProxyPort();
+ String user = proxy.getProxyUsername();
+ String passwd = proxy.getProxyPassword();
+
+ try
+ {
+ socket=new Socket(proxy_host, proxy_port);
+ in=socket.getInputStream();
+ out=socket.getOutputStream();
+ socket.setTcpNoDelay(true);
+
+ byte[] buf=new byte[1024];
+ int index=0;
+
+ /*
+ 1) CONNECT
+
+ The client connects to the SOCKS server and sends a CONNECT request when
+ it wants to establish a connection to an application server. The client
+ includes in the request packet the IP address and the port number of the
+ destination host, and userid, in the following format.
+
+ +----+----+----+----+----+----+----+----+----+----+....+----+
+ | VN | CD | DSTPORT | DSTIP | USERID |NULL|
+ +----+----+----+----+----+----+----+----+----+----+....+----+
+ # of bytes: 1 1 2 4 variable 1
+
+ VN is the SOCKS protocol version number and should be 4. CD is the
+ SOCKS command code and should be 1 for CONNECT request. NULL is a byte
+ of all zero bits.
+ */
+
+ index=0;
+ buf[index++]=4;
+ buf[index++]=1;
+
+ buf[index++]=(byte)(port>>>8);
+ buf[index++]=(byte)(port&0xff);
+
+ try
+ {
+ InetAddress addr=InetAddress.getByName(host);
+ byte[] byteAddress = addr.getAddress();
+ for (int i = 0; i < byteAddress.length; i++)
+ {
+ buf[index++]=byteAddress[i];
+ }
+ }
+ catch(UnknownHostException uhe)
+ {
+ throw new ProxyException(ProxyInfo.ProxyType.SOCKS4,
+ uhe.toString(), uhe);
+ }
+
+ if(user!=null)
+ {
+ System.arraycopy(user.getBytes(), 0, buf, index, user.length());
+ index+=user.length();
+ }
+ buf[index++]=0;
+ out.write(buf, 0, index);
+
+ /*
+ The SOCKS server checks to see whether such a request should be granted
+ based on any combination of source IP address, destination IP address,
+ destination port number, the userid, and information it may obtain by
+ consulting IDENT, cf. RFC 1413. If the request is granted, the SOCKS
+ server makes a connection to the specified port of the destination host.
+ A reply packet is sent to the client when this connection is established,
+ or when the request is rejected or the operation fails.
+
+ +----+----+----+----+----+----+----+----+
+ | VN | CD | DSTPORT | DSTIP |
+ +----+----+----+----+----+----+----+----+
+ # of bytes: 1 1 2 4
+
+ VN is the version of the reply code and should be 0. CD is the result
+ code with one of the following values:
+
+ 90: request granted
+ 91: request rejected or failed
+ 92: request rejected becasue SOCKS server cannot connect to
+ identd on the client
+ 93: request rejected because the client program and identd
+ report different user-ids
+
+ The remaining fields are ignored.
+ */
+
+ int len=6;
+ int s=0;
+ while(s<len)
+ {
+ int i=in.read(buf, s, len-s);
+ if(i<=0)
+ {
+ throw new ProxyException(ProxyInfo.ProxyType.SOCKS4,
+ "stream is closed");
+ }
+ s+=i;
+ }
+ if(buf[0]!=0)
+ {
+ throw new ProxyException(ProxyInfo.ProxyType.SOCKS4,
+ "server returns VN "+buf[0]);
+ }
+ if(buf[1]!=90)
+ {
+ try
+ {
+ socket.close();
+ }
+ catch(Exception eee)
+ {
+ }
+ String message="ProxySOCKS4: server returns CD "+buf[1];
+ throw new ProxyException(ProxyInfo.ProxyType.SOCKS4,message);
+ }
+ byte[] temp = new byte[2];
+ in.read(temp, 0, 2);
+ return socket;
+ }
+ catch(RuntimeException e)
+ {
+ throw e;
+ }
+ catch(Exception e)
+ {
+ try
+ {
+ if(socket!=null)socket.close();
+ }
+ catch(Exception eee)
+ {
+ }
+ throw new ProxyException(ProxyInfo.ProxyType.SOCKS4, e.toString());
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/proxy/Socks5ProxySocketFactory.java b/src/org/jivesoftware/smack/proxy/Socks5ProxySocketFactory.java
new file mode 100644
index 0000000..23ef623
--- /dev/null
+++ b/src/org/jivesoftware/smack/proxy/Socks5ProxySocketFactory.java
@@ -0,0 +1,375 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.proxy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import javax.net.SocketFactory;
+
+/**
+ * Socket factory for Socks5 proxy
+ *
+ * @author Atul Aggarwal
+ */
+public class Socks5ProxySocketFactory
+ extends SocketFactory
+{
+ private ProxyInfo proxy;
+
+ public Socks5ProxySocketFactory(ProxyInfo proxy)
+ {
+ this.proxy = proxy;
+ }
+
+ public Socket createSocket(String host, int port)
+ throws IOException, UnknownHostException
+ {
+ return socks5ProxifiedSocket(host,port);
+ }
+
+ public Socket createSocket(String host ,int port, InetAddress localHost,
+ int localPort)
+ throws IOException, UnknownHostException
+ {
+
+ return socks5ProxifiedSocket(host,port);
+
+ }
+
+ public Socket createSocket(InetAddress host, int port)
+ throws IOException
+ {
+
+ return socks5ProxifiedSocket(host.getHostAddress(),port);
+
+ }
+
+ public Socket createSocket( InetAddress address, int port,
+ InetAddress localAddress, int localPort)
+ throws IOException
+ {
+
+ return socks5ProxifiedSocket(address.getHostAddress(),port);
+
+ }
+
+ private Socket socks5ProxifiedSocket(String host, int port)
+ throws IOException
+ {
+ Socket socket = null;
+ InputStream in = null;
+ OutputStream out = null;
+ String proxy_host = proxy.getProxyAddress();
+ int proxy_port = proxy.getProxyPort();
+ String user = proxy.getProxyUsername();
+ String passwd = proxy.getProxyPassword();
+
+ try
+ {
+ socket=new Socket(proxy_host, proxy_port);
+ in=socket.getInputStream();
+ out=socket.getOutputStream();
+
+ socket.setTcpNoDelay(true);
+
+ byte[] buf=new byte[1024];
+ int index=0;
+
+/*
+ +----+----------+----------+
+ |VER | NMETHODS | METHODS |
+ +----+----------+----------+
+ | 1 | 1 | 1 to 255 |
+ +----+----------+----------+
+
+ The VER field is set to X'05' for this version of the protocol. The
+ NMETHODS field contains the number of method identifier octets that
+ appear in the METHODS field.
+
+ The values currently defined for METHOD are:
+
+ o X'00' NO AUTHENTICATION REQUIRED
+ o X'01' GSSAPI
+ o X'02' USERNAME/PASSWORD
+ o X'03' to X'7F' IANA ASSIGNED
+ o X'80' to X'FE' RESERVED FOR PRIVATE METHODS
+ o X'FF' NO ACCEPTABLE METHODS
+*/
+
+ buf[index++]=5;
+
+ buf[index++]=2;
+ buf[index++]=0; // NO AUTHENTICATION REQUIRED
+ buf[index++]=2; // USERNAME/PASSWORD
+
+ out.write(buf, 0, index);
+
+/*
+ The server selects from one of the methods given in METHODS, and
+ sends a METHOD selection message:
+
+ +----+--------+
+ |VER | METHOD |
+ +----+--------+
+ | 1 | 1 |
+ +----+--------+
+*/
+ //in.read(buf, 0, 2);
+ fill(in, buf, 2);
+
+ boolean check=false;
+ switch((buf[1])&0xff)
+ {
+ case 0: // NO AUTHENTICATION REQUIRED
+ check=true;
+ break;
+ case 2: // USERNAME/PASSWORD
+ if(user==null || passwd==null)
+ {
+ break;
+ }
+
+/*
+ Once the SOCKS V5 server has started, and the client has selected the
+ Username/Password Authentication protocol, the Username/Password
+ subnegotiation begins. This begins with the client producing a
+ Username/Password request:
+
+ +----+------+----------+------+----------+
+ |VER | ULEN | UNAME | PLEN | PASSWD |
+ +----+------+----------+------+----------+
+ | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
+ +----+------+----------+------+----------+
+
+ The VER field contains the current version of the subnegotiation,
+ which is X'01'. The ULEN field contains the length of the UNAME field
+ that follows. The UNAME field contains the username as known to the
+ source operating system. The PLEN field contains the length of the
+ PASSWD field that follows. The PASSWD field contains the password
+ association with the given UNAME.
+*/
+ index=0;
+ buf[index++]=1;
+ buf[index++]=(byte)(user.length());
+ System.arraycopy(user.getBytes(), 0, buf, index,
+ user.length());
+ index+=user.length();
+ buf[index++]=(byte)(passwd.length());
+ System.arraycopy(passwd.getBytes(), 0, buf, index,
+ passwd.length());
+ index+=passwd.length();
+
+ out.write(buf, 0, index);
+
+/*
+ The server verifies the supplied UNAME and PASSWD, and sends the
+ following response:
+
+ +----+--------+
+ |VER | STATUS |
+ +----+--------+
+ | 1 | 1 |
+ +----+--------+
+
+ A STATUS field of X'00' indicates success. If the server returns a
+ `failure' (STATUS value other than X'00') status, it MUST close the
+ connection.
+*/
+ //in.read(buf, 0, 2);
+ fill(in, buf, 2);
+ if(buf[1]==0)
+ {
+ check=true;
+ }
+ break;
+ default:
+ }
+
+ if(!check)
+ {
+ try
+ {
+ socket.close();
+ }
+ catch(Exception eee)
+ {
+ }
+ throw new ProxyException(ProxyInfo.ProxyType.SOCKS5,
+ "fail in SOCKS5 proxy");
+ }
+
+/*
+ The SOCKS request is formed as follows:
+
+ +----+-----+-------+------+----------+----------+
+ |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+ +----+-----+-------+------+----------+----------+
+ | 1 | 1 | X'00' | 1 | Variable | 2 |
+ +----+-----+-------+------+----------+----------+
+
+ Where:
+
+ o VER protocol version: X'05'
+ o CMD
+ o CONNECT X'01'
+ o BIND X'02'
+ o UDP ASSOCIATE X'03'
+ o RSV RESERVED
+ o ATYP address type of following address
+ o IP V4 address: X'01'
+ o DOMAINNAME: X'03'
+ o IP V6 address: X'04'
+ o DST.ADDR desired destination address
+ o DST.PORT desired destination port in network octet
+ order
+*/
+
+ index=0;
+ buf[index++]=5;
+ buf[index++]=1; // CONNECT
+ buf[index++]=0;
+
+ byte[] hostb=host.getBytes();
+ int len=hostb.length;
+ buf[index++]=3; // DOMAINNAME
+ buf[index++]=(byte)(len);
+ System.arraycopy(hostb, 0, buf, index, len);
+ index+=len;
+ buf[index++]=(byte)(port>>>8);
+ buf[index++]=(byte)(port&0xff);
+
+ out.write(buf, 0, index);
+
+/*
+ The SOCKS request information is sent by the client as soon as it has
+ established a connection to the SOCKS server, and completed the
+ authentication negotiations. The server evaluates the request, and
+ returns a reply formed as follows:
+
+ +----+-----+-------+------+----------+----------+
+ |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+ +----+-----+-------+------+----------+----------+
+ | 1 | 1 | X'00' | 1 | Variable | 2 |
+ +----+-----+-------+------+----------+----------+
+
+ Where:
+
+ o VER protocol version: X'05'
+ o REP Reply field:
+ o X'00' succeeded
+ o X'01' general SOCKS server failure
+ o X'02' connection not allowed by ruleset
+ o X'03' Network unreachable
+ o X'04' Host unreachable
+ o X'05' Connection refused
+ o X'06' TTL expired
+ o X'07' Command not supported
+ o X'08' Address type not supported
+ o X'09' to X'FF' unassigned
+ o RSV RESERVED
+ o ATYP address type of following address
+ o IP V4 address: X'01'
+ o DOMAINNAME: X'03'
+ o IP V6 address: X'04'
+ o BND.ADDR server bound address
+ o BND.PORT server bound port in network octet order
+*/
+
+ //in.read(buf, 0, 4);
+ fill(in, buf, 4);
+
+ if(buf[1]!=0)
+ {
+ try
+ {
+ socket.close();
+ }
+ catch(Exception eee)
+ {
+ }
+ throw new ProxyException(ProxyInfo.ProxyType.SOCKS5,
+ "server returns "+buf[1]);
+ }
+
+ switch(buf[3]&0xff)
+ {
+ case 1:
+ //in.read(buf, 0, 6);
+ fill(in, buf, 6);
+ break;
+ case 3:
+ //in.read(buf, 0, 1);
+ fill(in, buf, 1);
+ //in.read(buf, 0, buf[0]+2);
+ fill(in, buf, (buf[0]&0xff)+2);
+ break;
+ case 4:
+ //in.read(buf, 0, 18);
+ fill(in, buf, 18);
+ break;
+ default:
+ }
+ return socket;
+
+ }
+ catch(RuntimeException e)
+ {
+ throw e;
+ }
+ catch(Exception e)
+ {
+ try
+ {
+ if(socket!=null)
+ {
+ socket.close();
+ }
+ }
+ catch(Exception eee)
+ {
+ }
+ String message="ProxySOCKS5: "+e.toString();
+ if(e instanceof Throwable)
+ {
+ throw new ProxyException(ProxyInfo.ProxyType.SOCKS5,message,
+ (Throwable)e);
+ }
+ throw new IOException(message);
+ }
+ }
+
+ private void fill(InputStream in, byte[] buf, int len)
+ throws IOException
+ {
+ int s=0;
+ while(s<len)
+ {
+ int i=in.read(buf, s, len-s);
+ if(i<=0)
+ {
+ throw new ProxyException(ProxyInfo.ProxyType.SOCKS5, "stream " +
+ "is closed");
+ }
+ s+=i;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/sasl/SASLAnonymous.java b/src/org/jivesoftware/smack/sasl/SASLAnonymous.java
new file mode 100644
index 0000000..a1b2c88
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLAnonymous.java
@@ -0,0 +1,62 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ *
+ * 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 java.io.IOException;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+
+/**
+ * Implementation of the SASL ANONYMOUS mechanism
+ *
+ * @author Jay Kline
+ */
+public class SASLAnonymous extends SASLMechanism {
+
+ public SASLAnonymous(SASLAuthentication saslAuthentication) {
+ super(saslAuthentication);
+ }
+
+ protected String getName() {
+ return "ANONYMOUS";
+ }
+
+ public void authenticate(String username, String host, CallbackHandler cbh) throws IOException {
+ authenticate();
+ }
+
+ public void authenticate(String username, String host, String password) throws IOException {
+ authenticate();
+ }
+
+ protected void authenticate() throws IOException {
+ // Send the authentication to the server
+ getSASLAuthentication().send(new AuthMechanism(getName(), null));
+ }
+
+ public void challengeReceived(String challenge) throws IOException {
+ // Build the challenge response stanza encoding the response text
+ // and send the authentication to the server
+ getSASLAuthentication().send(new Response());
+ }
+
+
+}
diff --git a/src/org/jivesoftware/smack/sasl/SASLCramMD5Mechanism.java b/src/org/jivesoftware/smack/sasl/SASLCramMD5Mechanism.java
new file mode 100644
index 0000000..82d218f
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLCramMD5Mechanism.java
@@ -0,0 +1,38 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ *
+ * 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 CRAM-MD5 mechanism
+ *
+ * @author Jay Kline
+ */
+public class SASLCramMD5Mechanism extends SASLMechanism {
+
+ public SASLCramMD5Mechanism(SASLAuthentication saslAuthentication) {
+ super(saslAuthentication);
+ }
+
+ protected String getName() {
+ return "CRAM-MD5";
+ }
+}
diff --git a/src/org/jivesoftware/smack/sasl/SASLDigestMD5Mechanism.java b/src/org/jivesoftware/smack/sasl/SASLDigestMD5Mechanism.java
new file mode 100644
index 0000000..7af65fb
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLDigestMD5Mechanism.java
@@ -0,0 +1,38 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ *
+ * 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 DIGEST-MD5 mechanism
+ *
+ * @author Jay Kline
+ */
+public class SASLDigestMD5Mechanism extends SASLMechanism {
+
+ public SASLDigestMD5Mechanism(SASLAuthentication saslAuthentication) {
+ super(saslAuthentication);
+ }
+
+ protected String getName() {
+ return "DIGEST-MD5";
+ }
+}
diff --git a/src/org/jivesoftware/smack/sasl/SASLExternalMechanism.java b/src/org/jivesoftware/smack/sasl/SASLExternalMechanism.java
new file mode 100644
index 0000000..dff18fb
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLExternalMechanism.java
@@ -0,0 +1,59 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ *
+ * 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 EXTERNAL mechanism.
+ *
+ * To effectively use this mechanism, Java must be configured to properly
+ * supply a client SSL certificate (of some sort) to the server. It is up
+ * to the implementer to determine how to do this. Here is one method:
+ *
+ * Create a java keystore with your SSL certificate in it:
+ * keytool -genkey -alias username -dname "cn=username,ou=organizationalUnit,o=organizationaName,l=locality,s=state,c=country"
+ *
+ * Next, set the System Properties:
+ * <ul>
+ * <li>javax.net.ssl.keyStore to the location of the keyStore
+ * <li>javax.net.ssl.keyStorePassword to the password of the keyStore
+ * <li>javax.net.ssl.trustStore to the location of the trustStore
+ * <li>javax.net.ssl.trustStorePassword to the the password of the trustStore
+ * </ul>
+ *
+ * Then, when the server requests or requires the client certificate, java will
+ * simply provide the one in the keyStore.
+ *
+ * Also worth noting is the EXTERNAL mechanism in Smack is not enabled by default.
+ * To enable it, the implementer will need to call SASLAuthentication.supportSASLMechamism("EXTERNAL");
+ *
+ * @author Jay Kline
+ */
+public class SASLExternalMechanism extends SASLMechanism {
+
+ public SASLExternalMechanism(SASLAuthentication saslAuthentication) {
+ super(saslAuthentication);
+ }
+
+ protected String getName() {
+ return "EXTERNAL";
+ }
+}
diff --git a/src/org/jivesoftware/smack/sasl/SASLFacebookConnect.java b/src/org/jivesoftware/smack/sasl/SASLFacebookConnect.java
new file mode 100644
index 0000000..3126d83
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLFacebookConnect.java
@@ -0,0 +1,201 @@
+package org.jivesoftware.smack.sasl;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import de.measite.smack.Sasl;
+
+import org.jivesoftware.smack.SASLAuthentication;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.sasl.SASLMechanism;
+import org.jivesoftware.smack.util.Base64;
+
+/**
+ * This class is originally from http://code.google.com/p/fbgc/source/browse/trunk/daemon/src/main/java/org/albino/mechanisms/FacebookConnectSASLMechanism.java
+ * I just adapted to match the SMACK package scheme and
+ */
+public class SASLFacebookConnect extends SASLMechanism {
+
+ private String sessionKey = "";
+ private String sessionSecret = "";
+ private String apiKey = "";
+
+ static{
+ SASLAuthentication.registerSASLMechanism("X-FACEBOOK-PLATFORM",
+ SASLFacebookConnect.class);
+ SASLAuthentication.supportSASLMechanism("X-FACEBOOK-PLATFORM", 0);
+ }
+
+ public SASLFacebookConnect(SASLAuthentication saslAuthentication) {
+ super(saslAuthentication);
+ }
+
+ // protected void authenticate() throws IOException, XMPPException {
+ // String[] mechanisms = { getName() };
+ // Map<String, String> props = new HashMap<String, String>();
+ // sc = Sasl.createSaslClient(mechanisms, null, "xmpp", hostname, props,
+ // this);
+ //
+ // super.authenticate();
+ // }
+
+ protected void authenticate() throws IOException, XMPPException {
+ final StringBuilder stanza = new StringBuilder();
+ stanza.append("<auth mechanism=\"").append(getName());
+ stanza.append("\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
+ stanza.append("</auth>");
+
+ // Send the authentication to the server
+ getSASLAuthentication().send(new Packet(){
+
+ @Override
+ public String toXML() {
+ return stanza.toString();
+ }
+
+ });
+ }
+
+ public void authenticate(String apiKeyAndSessionKey, String host, String sessionSecret)
+ throws IOException, XMPPException {
+
+ if(apiKeyAndSessionKey==null || sessionSecret==null)
+ throw new IllegalStateException("Invalid parameters!");
+
+ String[] keyArray = apiKeyAndSessionKey.split("\\|");
+
+ if(keyArray==null || keyArray.length != 2)
+ throw new IllegalStateException("Api key or session key is not present!");
+
+ this.apiKey = keyArray[0];
+ this.sessionKey = keyArray[1];
+ this.sessionSecret = sessionSecret;
+
+ this.authenticationId = sessionKey;
+ this.password = sessionSecret;
+ this.hostname = host;
+
+ String[] mechanisms = { "DIGEST-MD5" };
+ Map<String, String> props = new HashMap<String, String>();
+ sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, this);
+ authenticate();
+ }
+
+ public void authenticate(String username, String host, CallbackHandler cbh)
+ throws IOException, XMPPException {
+ String[] mechanisms = { "DIGEST-MD5" };
+ Map<String, String> props = new HashMap<String, String>();
+ sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, cbh);
+ authenticate();
+ }
+
+ protected String getName() {
+ return "X-FACEBOOK-PLATFORM";
+ }
+
+ public void challengeReceived(String challenge) throws IOException {
+ // Build the challenge response stanza encoding the response text
+ final StringBuilder stanza = new StringBuilder();
+
+ byte response[] = null;
+ if (challenge != null) {
+ String decodedResponse = new String(Base64.decode(challenge));
+ Map<String, String> parameters = getQueryMap(decodedResponse);
+
+ String version = "1.0";
+ String nonce = parameters.get("nonce");
+ String method = parameters.get("method");
+
+ Long callId = new GregorianCalendar().getTimeInMillis()/1000;
+
+ String sig = "api_key="+apiKey
+ +"call_id="+callId
+ +"method="+method
+ +"nonce="+nonce
+ +"session_key="+sessionKey
+ +"v="+version
+ +sessionSecret;
+
+ try {
+ sig = MD5(sig);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException(e);
+ }
+
+ String composedResponse = "api_key="+apiKey+"&"
+ +"call_id="+callId+"&"
+ +"method="+method+"&"
+ +"nonce="+nonce+"&"
+ +"session_key="+sessionKey+"&"
+ +"v="+version+"&"
+ +"sig="+sig;
+
+ response = composedResponse.getBytes();
+ }
+
+ String authenticationText="";
+
+ if (response != null) {
+ authenticationText = Base64.encodeBytes(response, Base64.DONT_BREAK_LINES);
+ }
+
+ stanza.append("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
+ stanza.append(authenticationText);
+ stanza.append("</response>");
+
+ // Send the authentication to the server
+ getSASLAuthentication().send(new Packet(){
+
+ @Override
+ public String toXML() {
+ return stanza.toString();
+ }
+
+ });
+ }
+
+ private Map<String, String> getQueryMap(String query) {
+ String[] params = query.split("&");
+ Map<String, String> map = new HashMap<String, String>();
+ for (String param : params) {
+ String name = param.split("=")[0];
+ String value = param.split("=")[1];
+ map.put(name, value);
+ }
+ return map;
+ }
+
+ private String convertToHex(byte[] data) {
+ StringBuffer buf = new StringBuffer();
+ for (int i = 0; i < data.length; i++) {
+ int halfbyte = (data[i] >>> 4) & 0x0F;
+ int two_halfs = 0;
+ do {
+ if ((0 <= halfbyte) && (halfbyte <= 9))
+ buf.append((char) ('0' + halfbyte));
+ else
+ buf.append((char) ('a' + (halfbyte - 10)));
+ halfbyte = data[i] & 0x0F;
+ } while(two_halfs++ < 1);
+ }
+ return buf.toString();
+ }
+
+ public String MD5(String text) throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ MessageDigest md;
+ md = MessageDigest.getInstance("MD5");
+ byte[] md5hash = new byte[32];
+ md.update(text.getBytes("iso-8859-1"), 0, text.length());
+ md5hash = md.digest();
+ return convertToHex(md5hash);
+ }
+}
+
diff --git a/src/org/jivesoftware/smack/sasl/SASLGSSAPIMechanism.java b/src/org/jivesoftware/smack/sasl/SASLGSSAPIMechanism.java
new file mode 100644
index 0000000..e8a4967
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLGSSAPIMechanism.java
@@ -0,0 +1,89 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ *
+ * 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.XMPPException;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.HashMap;
+import de.measite.smack.Sasl;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+
+/**
+ * Implementation of the SASL GSSAPI mechanism
+ *
+ * @author Jay Kline
+ */
+public class SASLGSSAPIMechanism extends SASLMechanism {
+
+ public SASLGSSAPIMechanism(SASLAuthentication saslAuthentication) {
+ super(saslAuthentication);
+
+ System.setProperty("javax.security.auth.useSubjectCredsOnly","false");
+ System.setProperty("java.security.auth.login.config","gss.conf");
+
+ }
+
+ protected String getName() {
+ return "GSSAPI";
+ }
+
+ /**
+ * Builds and sends the <tt>auth</tt> stanza to the server.
+ * This overrides from the abstract class because the initial token
+ * needed for GSSAPI is binary, and not safe to put in a string, thus
+ * getAuthenticationText() cannot be used.
+ *
+ * @param username the username of the user being authenticated.
+ * @param host the hostname where the user account resides.
+ * @param cbh the CallbackHandler (not used with GSSAPI)
+ * @throws IOException If a network error occures while authenticating.
+ */
+ public void authenticate(String username, String host, CallbackHandler cbh) throws IOException, XMPPException {
+ String[] mechanisms = { getName() };
+ Map<String,String> props = new HashMap<String,String>();
+ props.put(Sasl.SERVER_AUTH,"TRUE");
+ sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props, cbh);
+ authenticate();
+ }
+
+ /**
+ * Builds and sends the <tt>auth</tt> stanza to the server.
+ * This overrides from the abstract class because the initial token
+ * needed for GSSAPI is binary, and not safe to put in a string, thus
+ * getAuthenticationText() cannot be used.
+ *
+ * @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 (ignored for GSSAPI)
+ * @throws IOException If a network error occures while authenticating.
+ */
+ public void authenticate(String username, String host, String password) throws IOException, XMPPException {
+ String[] mechanisms = { getName() };
+ Map<String,String> props = new HashMap<String, String>();
+ props.put(Sasl.SERVER_AUTH,"TRUE");
+ sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props, this);
+ authenticate();
+ }
+
+
+}
diff --git a/src/org/jivesoftware/smack/sasl/SASLMechanism.java b/src/org/jivesoftware/smack/sasl/SASLMechanism.java
new file mode 100644
index 0000000..3aeba86
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLMechanism.java
@@ -0,0 +1,323 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.XMPPException;
+import org.jivesoftware.smack.SASLAuthentication;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.HashMap;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.NameCallback;
+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
+import org.apache.harmony.javax.security.sasl.RealmCallback;
+import org.apache.harmony.javax.security.sasl.RealmChoiceCallback;
+import de.measite.smack.Sasl;
+import org.apache.harmony.javax.security.sasl.SaslClient;
+import org.apache.harmony.javax.security.sasl.SaslException;
+
+/**
+ * Base class for SASL mechanisms. Subclasses must implement these methods:
+ * <ul>
+ * <li>{@link #getName()} -- returns the common name of the SASL mechanism.</li>
+ * </ul>
+ * Subclasses will likely want to implement their own versions of these mthods:
+ * <li>{@link #authenticate(String, String, String)} -- Initiate authentication stanza using the
+ * deprecated method.</li>
+ * <li>{@link #authenticate(String, String, CallbackHandler)} -- Initiate authentication stanza
+ * using the CallbackHandler method.</li>
+ * <li>{@link #challengeReceived(String)} -- Handle a challenge from the server.</li>
+ * </ul>
+ *
+ * @author Jay Kline
+ */
+public abstract class SASLMechanism implements CallbackHandler {
+
+ private SASLAuthentication saslAuthentication;
+ protected SaslClient sc;
+ protected String authenticationId;
+ protected String password;
+ protected String hostname;
+
+
+ public SASLMechanism(SASLAuthentication saslAuthentication) {
+ this.saslAuthentication = saslAuthentication;
+ }
+
+ /**
+ * Builds and sends the <tt>auth</tt> stanza to the server. Note that this method of
+ * authentication is not recommended, since it is very inflexable. Use
+ * {@link #authenticate(String, String, CallbackHandler)} whenever possible.
+ *
+ * @param username the username of the user being authenticated.
+ * @param host the hostname where the user account resides.
+ * @param password the password for this account.
+ * @throws IOException If a network error occurs while authenticating.
+ * @throws XMPPException If a protocol error occurs or the user is not authenticated.
+ */
+ public void authenticate(String username, String host, String password) throws IOException, XMPPException {
+ //Since we were not provided with a CallbackHandler, we will use our own with the given
+ //information
+
+ //Set the authenticationID as the username, since they must be the same in this case.
+ this.authenticationId = username;
+ this.password = password;
+ this.hostname = host;
+
+ String[] mechanisms = { getName() };
+ Map<String,String> props = new HashMap<String,String>();
+ sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props, this);
+ authenticate();
+ }
+
+ /**
+ * Builds and sends the <tt>auth</tt> stanza to the server. The callback handler will handle
+ * any additional information, such as the authentication ID or realm, if it is needed.
+ *
+ * @param username the username of the user being authenticated.
+ * @param host the hostname where the user account resides.
+ * @param cbh the CallbackHandler to obtain user information.
+ * @throws IOException If a network error occures while authenticating.
+ * @throws XMPPException If a protocol error occurs or the user is not authenticated.
+ */
+ public void authenticate(String username, String host, CallbackHandler cbh) throws IOException, XMPPException {
+ String[] mechanisms = { getName() };
+ Map<String,String> props = new HashMap<String,String>();
+ sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props, cbh);
+ authenticate();
+ }
+
+ protected void authenticate() throws IOException, XMPPException {
+ String authenticationText = null;
+ try {
+ if(sc.hasInitialResponse()) {
+ byte[] response = sc.evaluateChallenge(new byte[0]);
+ authenticationText = StringUtils.encodeBase64(response, false);
+ }
+ } catch (SaslException e) {
+ throw new XMPPException("SASL authentication failed", e);
+ }
+
+ // Send the authentication to the server
+ getSASLAuthentication().send(new AuthMechanism(getName(), authenticationText));
+ }
+
+
+ /**
+ * 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.
+ * @throws IOException if an exception sending the response occurs.
+ */
+ public void challengeReceived(String challenge) throws IOException {
+ byte response[];
+ if(challenge != null) {
+ response = sc.evaluateChallenge(StringUtils.decodeBase64(challenge));
+ } else {
+ response = sc.evaluateChallenge(new byte[0]);
+ }
+
+ Packet responseStanza;
+ if (response == null) {
+ responseStanza = new Response();
+ }
+ else {
+ responseStanza = new Response(StringUtils.encodeBase64(response, false));
+ }
+
+ // Send the authentication to the server
+ getSASLAuthentication().send(responseStanza);
+ }
+
+ /**
+ * Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or GSSAPI.
+ *
+ * @return the common name of the SASL mechanism.
+ */
+ protected abstract String getName();
+
+
+ protected SASLAuthentication getSASLAuthentication() {
+ return saslAuthentication;
+ }
+
+ /**
+ *
+ */
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+ for (int i = 0; i < callbacks.length; i++) {
+ if (callbacks[i] instanceof NameCallback) {
+ NameCallback ncb = (NameCallback)callbacks[i];
+ ncb.setName(authenticationId);
+ } else if(callbacks[i] instanceof PasswordCallback) {
+ PasswordCallback pcb = (PasswordCallback)callbacks[i];
+ pcb.setPassword(password.toCharArray());
+ } else if(callbacks[i] instanceof RealmCallback) {
+ RealmCallback rcb = (RealmCallback)callbacks[i];
+ rcb.setText(hostname);
+ } else if(callbacks[i] instanceof RealmChoiceCallback){
+ //unused
+ //RealmChoiceCallback rccb = (RealmChoiceCallback)callbacks[i];
+ } else {
+ throw new UnsupportedCallbackException(callbacks[i]);
+ }
+ }
+ }
+
+ /**
+ * Initiating SASL authentication by select a mechanism.
+ */
+ public class AuthMechanism extends Packet {
+ final private String name;
+ final private String authenticationText;
+
+ public AuthMechanism(String name, String authenticationText) {
+ if (name == null) {
+ throw new NullPointerException("SASL mechanism name shouldn't be null.");
+ }
+ this.name = name;
+ this.authenticationText = authenticationText;
+ }
+
+ public String toXML() {
+ StringBuilder stanza = new StringBuilder();
+ stanza.append("<auth mechanism=\"").append(name);
+ stanza.append("\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
+ if (authenticationText != null &&
+ authenticationText.trim().length() > 0) {
+ stanza.append(authenticationText);
+ }
+ stanza.append("</auth>");
+ return stanza.toString();
+ }
+ }
+
+ /**
+ * A SASL challenge stanza.
+ */
+ public static class Challenge extends Packet {
+ final private String data;
+
+ public Challenge(String data) {
+ this.data = data;
+ }
+
+ public String toXML() {
+ StringBuilder stanza = new StringBuilder();
+ stanza.append("<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
+ if (data != null &&
+ data.trim().length() > 0) {
+ stanza.append(data);
+ }
+ stanza.append("</challenge>");
+ return stanza.toString();
+ }
+ }
+
+ /**
+ * A SASL response stanza.
+ */
+ public class Response extends Packet {
+ final private String authenticationText;
+
+ public Response() {
+ authenticationText = null;
+ }
+
+ public Response(String authenticationText) {
+ if (authenticationText == null || authenticationText.trim().length() == 0) {
+ this.authenticationText = null;
+ }
+ else {
+ this.authenticationText = authenticationText;
+ }
+ }
+
+ public String toXML() {
+ StringBuilder stanza = new StringBuilder();
+ stanza.append("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
+ if (authenticationText != null) {
+ stanza.append(authenticationText);
+ }
+ stanza.append("</response>");
+ return stanza.toString();
+ }
+ }
+
+ /**
+ * A SASL success stanza.
+ */
+ public static class Success extends Packet {
+ final private String data;
+
+ public Success(String data) {
+ this.data = data;
+ }
+
+ public String toXML() {
+ StringBuilder stanza = new StringBuilder();
+ stanza.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
+ if (data != null &&
+ data.trim().length() > 0) {
+ stanza.append(data);
+ }
+ stanza.append("</success>");
+ return stanza.toString();
+ }
+ }
+
+ /**
+ * A SASL failure stanza.
+ */
+ public static class Failure extends Packet {
+ final private String condition;
+
+ public Failure(String condition) {
+ this.condition = condition;
+ }
+
+ /**
+ * Get the SASL related error condition.
+ *
+ * @return the SASL related error condition.
+ */
+ public String getCondition() {
+ return condition;
+ }
+
+ public String toXML() {
+ StringBuilder stanza = new StringBuilder();
+ stanza.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
+ if (condition != null &&
+ condition.trim().length() > 0) {
+ stanza.append("<").append(condition).append("/>");
+ }
+ stanza.append("</failure>");
+ return stanza.toString();
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java b/src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java
new file mode 100644
index 0000000..cd973eb
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java
@@ -0,0 +1,34 @@
+/**
+ *
+ * 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 mechanism
+ *
+ * @author Jay Kline
+ */
+public class SASLPlainMechanism extends SASLMechanism {
+
+ public SASLPlainMechanism(SASLAuthentication saslAuthentication) {
+ super(saslAuthentication);
+ }
+
+ protected String getName() {
+ return "PLAIN";
+ }
+}
diff --git a/src/org/jivesoftware/smack/sasl/package.html b/src/org/jivesoftware/smack/sasl/package.html
new file mode 100644
index 0000000..1e8cfb7
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/package.html
@@ -0,0 +1 @@
+<body>SASL Mechanisms.</body> \ No newline at end of file
diff --git a/src/org/jivesoftware/smack/util/Base32Encoder.java b/src/org/jivesoftware/smack/util/Base32Encoder.java
new file mode 100644
index 0000000..0a4ea21
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/Base32Encoder.java
@@ -0,0 +1,184 @@
+/**
+ * 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.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * Base32 string encoding is useful for when filenames case-insensitive filesystems are encoded.
+ * Base32 representation takes roughly 20% more space then Base64.
+ *
+ * @author Florian Schmaus
+ * Based on code by Brian Wellington (bwelling@xbill.org)
+ * @see <a href="http://en.wikipedia.org/wiki/Base32">Base32 Wikipedia entry<a>
+ *
+ */
+public class Base32Encoder implements StringEncoder {
+
+ private static Base32Encoder instance = new Base32Encoder();
+ private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ2345678";
+
+ private Base32Encoder() {
+ // Use getInstance()
+ }
+
+ public static Base32Encoder getInstance() {
+ return instance;
+ }
+
+ @Override
+ public String decode(String str) {
+ ByteArrayOutputStream bs = new ByteArrayOutputStream();
+ byte[] raw = str.getBytes();
+ for (int i = 0; i < raw.length; i++) {
+ char c = (char) raw[i];
+ if (!Character.isWhitespace(c)) {
+ c = Character.toUpperCase(c);
+ bs.write((byte) c);
+ }
+ }
+
+ while (bs.size() % 8 != 0)
+ bs.write('8');
+
+ byte[] in = bs.toByteArray();
+
+ bs.reset();
+ DataOutputStream ds = new DataOutputStream(bs);
+
+ for (int i = 0; i < in.length / 8; i++) {
+ short[] s = new short[8];
+ int[] t = new int[5];
+
+ int padlen = 8;
+ for (int j = 0; j < 8; j++) {
+ char c = (char) in[i * 8 + j];
+ if (c == '8')
+ break;
+ s[j] = (short) ALPHABET.indexOf(in[i * 8 + j]);
+ if (s[j] < 0)
+ return null;
+ padlen--;
+ }
+ int blocklen = paddingToLen(padlen);
+ if (blocklen < 0)
+ return null;
+
+ // all 5 bits of 1st, high 3 (of 5) of 2nd
+ t[0] = (s[0] << 3) | s[1] >> 2;
+ // lower 2 of 2nd, all 5 of 3rd, high 1 of 4th
+ t[1] = ((s[1] & 0x03) << 6) | (s[2] << 1) | (s[3] >> 4);
+ // lower 4 of 4th, high 4 of 5th
+ t[2] = ((s[3] & 0x0F) << 4) | ((s[4] >> 1) & 0x0F);
+ // lower 1 of 5th, all 5 of 6th, high 2 of 7th
+ t[3] = (s[4] << 7) | (s[5] << 2) | (s[6] >> 3);
+ // lower 3 of 7th, all of 8th
+ t[4] = ((s[6] & 0x07) << 5) | s[7];
+
+ try {
+ for (int j = 0; j < blocklen; j++)
+ ds.writeByte((byte) (t[j] & 0xFF));
+ } catch (IOException e) {
+ }
+ }
+
+ return new String(bs.toByteArray());
+ }
+
+ @Override
+ public String encode(String str) {
+ byte[] b = str.getBytes();
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+
+ for (int i = 0; i < (b.length + 4) / 5; i++) {
+ short s[] = new short[5];
+ int t[] = new int[8];
+
+ int blocklen = 5;
+ for (int j = 0; j < 5; j++) {
+ if ((i * 5 + j) < b.length)
+ s[j] = (short) (b[i * 5 + j] & 0xFF);
+ else {
+ s[j] = 0;
+ blocklen--;
+ }
+ }
+ int padlen = lenToPadding(blocklen);
+
+ // convert the 5 byte block into 8 characters (values 0-31).
+
+ // upper 5 bits from first byte
+ t[0] = (byte) ((s[0] >> 3) & 0x1F);
+ // lower 3 bits from 1st byte, upper 2 bits from 2nd.
+ t[1] = (byte) (((s[0] & 0x07) << 2) | ((s[1] >> 6) & 0x03));
+ // bits 5-1 from 2nd.
+ t[2] = (byte) ((s[1] >> 1) & 0x1F);
+ // lower 1 bit from 2nd, upper 4 from 3rd
+ t[3] = (byte) (((s[1] & 0x01) << 4) | ((s[2] >> 4) & 0x0F));
+ // lower 4 from 3rd, upper 1 from 4th.
+ t[4] = (byte) (((s[2] & 0x0F) << 1) | ((s[3] >> 7) & 0x01));
+ // bits 6-2 from 4th
+ t[5] = (byte) ((s[3] >> 2) & 0x1F);
+ // lower 2 from 4th, upper 3 from 5th;
+ t[6] = (byte) (((s[3] & 0x03) << 3) | ((s[4] >> 5) & 0x07));
+ // lower 5 from 5th;
+ t[7] = (byte) (s[4] & 0x1F);
+
+ // write out the actual characters.
+ for (int j = 0; j < t.length - padlen; j++) {
+ char c = ALPHABET.charAt(t[j]);
+ os.write(c);
+ }
+ }
+ return new String(os.toByteArray());
+ }
+
+ private static int lenToPadding(int blocklen) {
+ switch (blocklen) {
+ case 1:
+ return 6;
+ case 2:
+ return 4;
+ case 3:
+ return 3;
+ case 4:
+ return 1;
+ case 5:
+ return 0;
+ default:
+ return -1;
+ }
+ }
+
+ private static int paddingToLen(int padlen) {
+ switch (padlen) {
+ case 6:
+ return 1;
+ case 4:
+ return 2;
+ case 3:
+ return 3;
+ case 1:
+ return 4;
+ case 0:
+ return 5;
+ default:
+ return -1;
+ }
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/util/Base64.java b/src/org/jivesoftware/smack/util/Base64.java
new file mode 100644
index 0000000..ba6eb37
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/Base64.java
@@ -0,0 +1,1689 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ */
+package org.jivesoftware.smack.util;
+
+/**
+ * <p>Encodes and decodes to and from Base64 notation.</p>
+ * This code was obtained from <a href="http://iharder.net/base64">http://iharder.net/base64</a></p>
+ *
+ *
+ * @author Robert Harder
+ * @author rob@iharder.net
+ * @version 2.2.1
+ */
+public 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;
+
+ /**
+ * Encode using Base64-like encoding that is URL- and Filename-safe as described
+ * in Section 4 of RFC3548:
+ * <a href="http://www.faqs.org/rfcs/rfc3548.html">http://www.faqs.org/rfcs/rfc3548.html</a>.
+ * It is important to note that data encoded this way is <em>not</em> officially valid Base64,
+ * or at the very least should not be called Base64 without also specifying that is
+ * was encoded using the URL- and Filename-safe dialect.
+ */
+ public final static int URL_SAFE = 16;
+
+
+ /**
+ * Encode using the special "ordered" dialect of Base64 described here:
+ * <a href="http://www.faqs.org/qa/rfcc-1940.html">http://www.faqs.org/qa/rfcc-1940.html</a>.
+ */
+ public final static int ORDERED = 32;
+
+
+/* ******** 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";
+
+
+ // 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
+
+
+/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */
+
+ /** The 64 valid Base64 values. */
+ //private final static byte[] ALPHABET;
+ /* Host platform me be something funny like EBCDIC, so we hardcode these values. */
+ private final static byte[] _STANDARD_ALPHABET =
+ {
+ (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)'/'
+ };
+
+
+ /**
+ * Translates a Base64 value to either its 6-bit reconstruction value
+ * or a negative number indicating some other meaning.
+ **/
+ private final static byte[] _STANDARD_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 */
+ };
+
+
+/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */
+
+ /**
+ * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548:
+ * <a href="http://www.faqs.org/rfcs/rfc3548.html">http://www.faqs.org/rfcs/rfc3548.html</a>.
+ * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash."
+ */
+ private final static byte[] _URL_SAFE_ALPHABET =
+ {
+ (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)'_'
+ };
+
+ /**
+ * Used in decoding URL- and Filename-safe dialects of Base64.
+ */
+ private final static byte[] _URL_SAFE_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
+ -9, // Plus sign at decimal 43
+ -9, // Decimal 44
+ 62, // Minus sign at decimal 45
+ -9, // Decimal 46
+ -9, // 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, // Decimal 91 - 94
+ 63, // Underscore at decimal 95
+ -9, // Decimal 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 */
+ };
+
+
+
+/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */
+
+ /**
+ * I don't get the point of this technique, but it is described here:
+ * <a href="http://www.faqs.org/qa/rfcc-1940.html">http://www.faqs.org/qa/rfcc-1940.html</a>.
+ */
+ private final static byte[] _ORDERED_ALPHABET =
+ {
+ (byte)'-',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4',
+ (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9',
+ (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)'_',
+ (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'
+ };
+
+ /**
+ * Used in decoding the "ordered" dialect of Base64.
+ */
+ private final static byte[] _ORDERED_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
+ -9, // Plus sign at decimal 43
+ -9, // Decimal 44
+ 0, // Minus sign at decimal 45
+ -9, // Decimal 46
+ -9, // Slash at decimal 47
+ 1,2,3,4,5,6,7,8,9,10, // Numbers zero through nine
+ -9,-9,-9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9,-9,-9, // Decimal 62 - 64
+ 11,12,13,14,15,16,17,18,19,20,21,22,23, // Letters 'A' through 'M'
+ 24,25,26,27,28,29,30,31,32,33,34,35,36, // Letters 'N' through 'Z'
+ -9,-9,-9,-9, // Decimal 91 - 94
+ 37, // Underscore at decimal 95
+ -9, // Decimal 96
+ 38,39,40,41,42,43,44,45,46,47,48,49,50, // Letters 'a' through 'm'
+ 51,52,53,54,55,56,57,58,59,60,61,62,63, // 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 */
+ };
+
+
+/* ******** D E T E R M I N E W H I C H A L H A B E T ******** */
+
+
+ /**
+ * Returns one of the _SOMETHING_ALPHABET byte arrays depending on
+ * the options specified.
+ * It's possible, though silly, to specify ORDERED and URLSAFE
+ * in which case one of them will be picked, though there is
+ * no guarantee as to which one will be picked.
+ */
+ private final static byte[] getAlphabet( int options )
+ {
+ if( (options & URL_SAFE) == URL_SAFE ) return _URL_SAFE_ALPHABET;
+ else if( (options & ORDERED) == ORDERED ) return _ORDERED_ALPHABET;
+ else return _STANDARD_ALPHABET;
+
+ } // end getAlphabet
+
+
+ /**
+ * Returns one of the _SOMETHING_DECODABET byte arrays depending on
+ * the options specified.
+ * It's possible, though silly, to specify ORDERED and URL_SAFE
+ * in which case one of them will be picked, though there is
+ * no guarantee as to which one will be picked.
+ */
+ private final static byte[] getDecodabet( int options )
+ {
+ if( (options & URL_SAFE) == URL_SAFE ) return _URL_SAFE_DECODABET;
+ else if( (options & ORDERED) == ORDERED ) return _ORDERED_DECODABET;
+ else return _STANDARD_DECODABET;
+
+ } // end getAlphabet
+
+
+
+ /** Defeats instantiation. */
+ private Base64(){}
+
+ /**
+ * Prints command line usage.
+ *
+ * @param msg A message to include with usage info.
+ */
+ private final static void usage( String msg )
+ {
+ System.err.println( msg );
+ System.err.println( "Usage: java Base64 -e|-d inputfile outputfile" );
+ } // end usage
+
+
+/* ******** 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, int options )
+ {
+ encode3to4( threeBytes, 0, numSigBytes, b4, 0, options );
+ return b4;
+ } // end encode3to4
+
+
+ /**
+ * <p>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>.</p>
+ * <p>This is the lowest level of the encoding methods with
+ * all possible parameters.</p>
+ *
+ * @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, int options )
+ {
+ byte[] ALPHABET = getAlphabet( options );
+
+ // 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 | options );
+
+ // 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; alphabet type is pulled from this (standard, url-safe, ordered)
+ * @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 | options );
+ 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, options );
+
+ 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, options );
+ 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.
+ * <p>This is the lowest level of the decoding methods with
+ * all possible parameters.</p>
+ *
+ *
+ * @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
+ * @param options alphabet type is pulled from this (standard, url-safe, ordered)
+ * @return the number of decoded bytes converted
+ * @since 1.3
+ */
+ private static int decode4to3( byte[] source, int srcOffset, byte[] destination, int destOffset, int options )
+ {
+ byte[] DECODABET = getDecodabet( options );
+
+ // 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;
+ } // end 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 options )
+ {
+ byte[] DECODABET = getDecodabet( options );
+
+ 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, options );
+ 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 )
+ {
+ return decode( s, NO_OPTIONS );
+ }
+
+
+ /**
+ * Decodes data from Base64 notation, automatically
+ * detecting gzip-compressed data and decompressing it.
+ *
+ * @param s the string to decode
+ * @param options encode options such as URL_SAFE
+ * @return the decoded data
+ * @since 1.4
+ */
+ public static byte[] decode( String s, int options )
+ {
+ 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, options );
+
+
+ // 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[ Math.max((int)(file.length() * 1.4),40) ]; // Need max() for math on small files (v2.2.1)
+ 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
+
+ /**
+ * Reads <tt>infile</tt> and encodes it to <tt>outfile</tt>.
+ *
+ * @param infile Input file
+ * @param outfile Output file
+ * @since 2.2
+ */
+ public static void encodeFileToFile( String infile, String outfile )
+ {
+ String encoded = Base64.encodeFromFile( infile );
+ java.io.OutputStream out = null;
+ try{
+ out = new java.io.BufferedOutputStream(
+ new java.io.FileOutputStream( outfile ) );
+ out.write( encoded.getBytes("US-ASCII") ); // Strict, 7-bit output.
+ } // end try
+ catch( java.io.IOException ex ) {
+ ex.printStackTrace();
+ } // end catch
+ finally {
+ try { out.close(); }
+ catch( Exception ex ){}
+ } // end finally
+ } // end encodeFileToFile
+
+
+ /**
+ * Reads <tt>infile</tt> and decodes it to <tt>outfile</tt>.
+ *
+ * @param infile Input file
+ * @param outfile Output file
+ * @since 2.2
+ */
+ public static void decodeFileToFile( String infile, String outfile )
+ {
+ byte[] decoded = Base64.decodeFromFile( infile );
+ java.io.OutputStream out = null;
+ try{
+ out = new java.io.BufferedOutputStream(
+ new java.io.FileOutputStream( outfile ) );
+ out.write( decoded );
+ } // end try
+ catch( java.io.IOException ex ) {
+ ex.printStackTrace();
+ } // end catch
+ finally {
+ try { out.close(); }
+ catch( Exception ex ){}
+ } // end finally
+ } // end decodeFileToFile
+
+
+ /* ******** 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
+ private int options; // Record options used to create the stream.
+ private byte[] alphabet; // Local copies to avoid extra method calls
+ private byte[] decodabet; // Local copies to avoid extra method calls
+
+
+ /**
+ * 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;
+ this.options = options; // Record for later, mostly to determine which alphabet to use
+ this.alphabet = getAlphabet(options);
+ this.decodabet = getDecodabet(options);
+ } // 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, options );
+ 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, options );
+ 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;
+ private int options; // Record for later
+ private byte[] alphabet; // Local copies to avoid extra method calls
+ private byte[] decodabet; // Local copies to avoid extra method calls
+
+ /**
+ * 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];
+ this.options = options;
+ this.alphabet = getAlphabet(options);
+ this.decodabet = getDecodabet(options);
+ } // 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, options ) );
+
+ 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, options );
+ 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, options ) );
+ 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/src/org/jivesoftware/smack/util/Base64Encoder.java b/src/org/jivesoftware/smack/util/Base64Encoder.java
new file mode 100644
index 0000000..d53c0ed
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/Base64Encoder.java
@@ -0,0 +1,42 @@
+/**
+ * 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;
+
+
+/**
+ * A Base 64 encoding implementation.
+ * @author Florian Schmaus
+ */
+public class Base64Encoder implements StringEncoder {
+
+ private static Base64Encoder instance = new Base64Encoder();
+
+ private Base64Encoder() {
+ // Use getInstance()
+ }
+
+ public static Base64Encoder getInstance() {
+ return instance;
+ }
+
+ public String encode(String s) {
+ return Base64.encodeBytes(s.getBytes());
+ }
+
+ public String decode(String s) {
+ return new String(Base64.decode(s));
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/util/Base64FileUrlEncoder.java b/src/org/jivesoftware/smack/util/Base64FileUrlEncoder.java
new file mode 100644
index 0000000..190b374
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/Base64FileUrlEncoder.java
@@ -0,0 +1,48 @@
+/**
+ * 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;
+
+
+/**
+ * A Base 64 encoding implementation that generates filename and Url safe encodings.
+ *
+ * <p>
+ * Note: This does NOT produce standard Base 64 encodings, but a variant as defined in
+ * Section 4 of RFC3548:
+ * <a href="http://www.faqs.org/rfcs/rfc3548.html">http://www.faqs.org/rfcs/rfc3548.html</a>.
+ *
+ * @author Robin Collier
+ */
+public class Base64FileUrlEncoder implements StringEncoder {
+
+ private static Base64FileUrlEncoder instance = new Base64FileUrlEncoder();
+
+ private Base64FileUrlEncoder() {
+ // Use getInstance()
+ }
+
+ public static Base64FileUrlEncoder getInstance() {
+ return instance;
+ }
+
+ public String encode(String s) {
+ return Base64.encodeBytes(s.getBytes(), Base64.URL_SAFE);
+ }
+
+ public String decode(String s) {
+ return new String(Base64.decode(s, Base64.URL_SAFE));
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/util/Cache.java b/src/org/jivesoftware/smack/util/Cache.java
new file mode 100644
index 0000000..964ac23
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/Cache.java
@@ -0,0 +1,678 @@
+/**
+ * $Revision: 1456 $
+ * $Date: 2005-06-01 22:04:54 -0700 (Wed, 01 Jun 2005) $
+ *
+ * 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 org.jivesoftware.smack.util.collections.AbstractMapEntry;
+
+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<K, V> implements Map<K, V> {
+
+ /**
+ * The map the keys and values are stored in.
+ */
+ protected Map<K, CacheObject<V>> 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<K, CacheObject<V>>(103);
+
+ lastAccessedList = new LinkedList();
+ ageList = new LinkedList();
+ }
+
+ public synchronized V put(K key, V value) {
+ V oldValue = null;
+ // Delete an old entry if it exists.
+ if (map.containsKey(key)) {
+ oldValue = remove(key, true);
+ }
+
+ CacheObject<V> cacheObject = new CacheObject<V>(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 V get(Object key) {
+ // First, clear all entries that have been in cache longer than the
+ // maximum defined age.
+ deleteExpiredEntries();
+
+ CacheObject<V> 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 V 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 V remove(Object key, boolean internal) {
+ //noinspection SuspiciousMethodCalls
+ CacheObject<V> 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 (Object key : keys) {
+ remove(key);
+ }
+
+ // 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<V> values() {
+ // First, clear all entries that have been in cache longer than the
+ // maximum defined age.
+ deleteExpiredEntries();
+
+ return Collections.unmodifiableCollection(new AbstractCollection<V>() {
+ Collection<CacheObject<V>> values = map.values();
+ public Iterator<V> iterator() {
+ return new Iterator<V>() {
+ Iterator<CacheObject<V>> it = values.iterator();
+
+ public boolean hasNext() {
+ return it.hasNext();
+ }
+
+ public V next() {
+ return it.next().object;
+ }
+
+ public void remove() {
+ it.remove();
+ }
+ };
+ }
+
+ public int size() {
+ return values.size();
+ }
+ });
+ }
+
+ 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<? extends K, ? extends V> map) {
+ for (Entry<? extends K, ? extends V> entry : map.entrySet()) {
+ V 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) {
+ //noinspection unchecked
+ value = ((CacheObject<V>) 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();
+
+ //noinspection unchecked
+ CacheObject<V> cacheObject = new CacheObject<V>((V) value);
+
+ return map.containsValue(cacheObject);
+ }
+
+ public synchronized Set<Map.Entry<K, V>> 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 new AbstractSet<Map.Entry<K, V>>() {
+ private final Set<Map.Entry<K, CacheObject<V>>> set = map.entrySet();
+
+ public Iterator<Entry<K, V>> iterator() {
+ return new Iterator<Entry<K, V>>() {
+ private final Iterator<Entry<K, CacheObject<V>>> it = set.iterator();
+ public boolean hasNext() {
+ return it.hasNext();
+ }
+
+ public Entry<K, V> next() {
+ Map.Entry<K, CacheObject<V>> entry = it.next();
+ return new AbstractMapEntry<K, V>(entry.getKey(), entry.getValue().object) {
+ @Override
+ public V setValue(V value) {
+ throw new UnsupportedOperationException("Cannot set");
+ }
+ };
+ }
+
+ public void remove() {
+ it.remove();
+ }
+ };
+
+ }
+
+ public int size() {
+ return set.size();
+ }
+ };
+ }
+
+ public synchronized Set<K> 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<V> {
+
+ /**
+ * Underlying object wrapped by the CacheObject.
+ */
+ public V 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(V 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;
+
+ return object.equals(cacheObject.object);
+
+ }
+
+ 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.
+ * @return the node
+ */
+ 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;
+ StringBuilder buf = new StringBuilder();
+ 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/src/org/jivesoftware/smack/util/DNSUtil.java b/src/org/jivesoftware/smack/util/DNSUtil.java
new file mode 100644
index 0000000..628d8e8
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/DNSUtil.java
@@ -0,0 +1,229 @@
+/**
+ * $Revision: 1456 $
+ * $Date: 2005-06-01 22:04:54 -0700 (Wed, 01 Jun 2005) $
+ *
+ * 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.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.jivesoftware.smack.util.dns.DNSResolver;
+import org.jivesoftware.smack.util.dns.HostAddress;
+import org.jivesoftware.smack.util.dns.SRVRecord;
+
+/**
+ * Utility 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<String, List<HostAddress>> cache = new Cache<String, List<HostAddress>>(100, 1000*60*10);
+
+ private static DNSResolver dnsResolver = null;
+
+ /**
+ * Set the DNS resolver that should be used to perform DNS lookups.
+ *
+ * @param resolver
+ */
+ public static void setDNSResolver(DNSResolver resolver) {
+ dnsResolver = resolver;
+ }
+
+ /**
+ * Returns the current DNS resolved used to perform DNS lookups.
+ *
+ * @return
+ */
+ public static DNSResolver getDNSResolver() {
+ return dnsResolver;
+ }
+
+ /**
+ * Returns a list of HostAddresses under which 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 List of HostAddress, which encompasses the hostname and port that the
+ * XMPP server can be reached at for the specified domain.
+ */
+ public static List<HostAddress> resolveXMPPDomain(String domain) {
+ return resolveDomain(domain, 'c');
+ }
+
+ /**
+ * Returns a list of HostAddresses under which 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 List of HostAddress, which encompasses the hostname and port that the
+ * XMPP server can be reached at for the specified domain.
+ */
+ public static List<HostAddress> resolveXMPPServerDomain(String domain) {
+ return resolveDomain(domain, 's');
+ }
+
+ private static List<HostAddress> resolveDomain(String domain, char keyPrefix) {
+ // Prefix the key with 's' to distinguish him from the client domain lookups
+ String key = keyPrefix + domain;
+ // Return item from cache if it exists.
+ if (cache.containsKey(key)) {
+ List<HostAddress> addresses = cache.get(key);
+ if (addresses != null) {
+ return addresses;
+ }
+ }
+
+ if (dnsResolver == null)
+ throw new IllegalStateException("No DNS resolver active.");
+
+ List<HostAddress> addresses = new ArrayList<HostAddress>();
+
+ // Step one: Do SRV lookups
+ String srvDomain;
+ if (keyPrefix == 's') {
+ srvDomain = "_xmpp-server._tcp." + domain;
+ } else if (keyPrefix == 'c') {
+ srvDomain = "_xmpp-client._tcp." + domain;
+ } else {
+ srvDomain = domain;
+ }
+ List<SRVRecord> srvRecords = dnsResolver.lookupSRVRecords(srvDomain);
+ List<HostAddress> sortedRecords = sortSRVRecords(srvRecords);
+ if (sortedRecords != null)
+ addresses.addAll(sortedRecords);
+
+ // Step two: Add the hostname to the end of the list
+ addresses.add(new HostAddress(domain));
+
+ // Add item to cache.
+ cache.put(key, addresses);
+
+ return addresses;
+ }
+
+ /**
+ * Sort a given list of SRVRecords as described in RFC 2782
+ * Note that we follow the RFC with one exception. In a group of the same priority, only the first entry
+ * is calculated by random. The others are ore simply ordered by their priority.
+ *
+ * @param records
+ * @return
+ */
+ protected static List<HostAddress> sortSRVRecords(List<SRVRecord> records) {
+ // RFC 2782, Usage rules: "If there is precisely one SRV RR, and its Target is "."
+ // (the root domain), abort."
+ if (records.size() == 1 && records.get(0).getFQDN().equals("."))
+ return null;
+
+ // sorting the records improves the performance of the bisection later
+ Collections.sort(records);
+
+ // create the priority buckets
+ SortedMap<Integer, List<SRVRecord>> buckets = new TreeMap<Integer, List<SRVRecord>>();
+ for (SRVRecord r : records) {
+ Integer priority = r.getPriority();
+ List<SRVRecord> bucket = buckets.get(priority);
+ // create the list of SRVRecords if it doesn't exist
+ if (bucket == null) {
+ bucket = new LinkedList<SRVRecord>();
+ buckets.put(priority, bucket);
+ }
+ bucket.add(r);
+ }
+
+ List<HostAddress> res = new ArrayList<HostAddress>(records.size());
+
+ for (Integer priority : buckets.keySet()) {
+ List<SRVRecord> bucket = buckets.get(priority);
+ int bucketSize;
+ while ((bucketSize = bucket.size()) > 0) {
+ int[] totals = new int[bucket.size()];
+ int running_total = 0;
+ int count = 0;
+ int zeroWeight = 1;
+
+ for (SRVRecord r : bucket) {
+ if (r.getWeight() > 0)
+ zeroWeight = 0;
+ }
+
+ for (SRVRecord r : bucket) {
+ running_total += (r.getWeight() + zeroWeight);
+ totals[count] = running_total;
+ count++;
+ }
+ int selectedPos;
+ if (running_total == 0) {
+ // If running total is 0, then all weights in this priority
+ // group are 0. So we simply select one of the weights randomly
+ // as the other 'normal' algorithm is unable to handle this case
+ selectedPos = (int) (Math.random() * bucketSize);
+ } else {
+ double rnd = Math.random() * running_total;
+ selectedPos = bisect(totals, rnd);
+ }
+ // add the SRVRecord that was randomly chosen on it's weight
+ // to the start of the result list
+ SRVRecord chosenSRVRecord = bucket.remove(selectedPos);
+ res.add(chosenSRVRecord);
+ }
+ }
+
+ return res;
+ }
+
+ // TODO this is not yet really bisection just a stupid linear search
+ private static int bisect(int[] array, double value) {
+ int pos = 0;
+ for (int element : array) {
+ if (value < element)
+ break;
+ pos++;
+ }
+ return pos;
+ }
+} \ No newline at end of file
diff --git a/src/org/jivesoftware/smack/util/DateFormatType.java b/src/org/jivesoftware/smack/util/DateFormatType.java
new file mode 100644
index 0000000..9253038
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/DateFormatType.java
@@ -0,0 +1,65 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2013 Robin Collier.
+ *
+ * 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.text.SimpleDateFormat;
+
+/**
+ * Defines the various date and time profiles used in XMPP along with their associated formats.
+ *
+ * @author Robin Collier
+ *
+ */
+public enum DateFormatType {
+ // @formatter:off
+ XEP_0082_DATE_PROFILE("yyyy-MM-dd"),
+ XEP_0082_DATETIME_PROFILE("yyyy-MM-dd'T'HH:mm:ssZ"),
+ XEP_0082_DATETIME_MILLIS_PROFILE("yyyy-MM-dd'T'HH:mm:ss.SSSZ"),
+ XEP_0082_TIME_PROFILE("hh:mm:ss"),
+ XEP_0082_TIME_ZONE_PROFILE("hh:mm:ssZ"),
+ XEP_0082_TIME_MILLIS_PROFILE("hh:mm:ss.SSS"),
+ XEP_0082_TIME_MILLIS_ZONE_PROFILE("hh:mm:ss.SSSZ"),
+ XEP_0091_DATETIME("yyyyMMdd'T'HH:mm:ss");
+ // @formatter:on
+
+ private String formatString;
+
+ private DateFormatType(String dateFormat) {
+ formatString = dateFormat;
+ }
+
+ /**
+ * Get the format string as defined in either XEP-0082 or XEP-0091.
+ *
+ * @return The defined string format for the date.
+ */
+ public String getFormatString() {
+ return formatString;
+ }
+
+ /**
+ * Create a {@link SimpleDateFormat} object with the format defined by {@link #getFormatString()}.
+ *
+ * @return A new date formatter.
+ */
+ public SimpleDateFormat createFormatter() {
+ return new SimpleDateFormat(getFormatString());
+ }
+}
diff --git a/src/org/jivesoftware/smack/util/ObservableReader.java b/src/org/jivesoftware/smack/util/ObservableReader.java
new file mode 100644
index 0000000..8c64508
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/ObservableReader.java
@@ -0,0 +1,118 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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<ReaderListener> listeners = new ArrayList<ReaderListener>();
+
+ 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/src/org/jivesoftware/smack/util/ObservableWriter.java b/src/org/jivesoftware/smack/util/ObservableWriter.java
new file mode 100644
index 0000000..90cabb6
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/ObservableWriter.java
@@ -0,0 +1,120 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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<WriterListener> listeners = new ArrayList<WriterListener>();
+
+ 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/src/org/jivesoftware/smack/util/PacketParserUtils.java b/src/org/jivesoftware/smack/util/PacketParserUtils.java
new file mode 100644
index 0000000..aacbad5
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/PacketParserUtils.java
@@ -0,0 +1,925 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.Authentication;
+import org.jivesoftware.smack.packet.Bind;
+import org.jivesoftware.smack.packet.DefaultPacketExtension;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.Registration;
+import org.jivesoftware.smack.packet.RosterPacket;
+import org.jivesoftware.smack.packet.StreamError;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smack.provider.ProviderManager;
+import org.jivesoftware.smack.sasl.SASLMechanism.Failure;
+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")));
+ String language = getLanguageAttribute(parser);
+
+ // determine message's default language
+ String defaultLanguage = null;
+ if (language != null && !"".equals(language.trim())) {
+ message.setLanguage(language);
+ defaultLanguage = language;
+ }
+ else {
+ defaultLanguage = Packet.getDefaultLanguage();
+ }
+
+ // 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 thread = null;
+ Map<String, Object> 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")) {
+ String xmlLang = getLanguageAttribute(parser);
+ if (xmlLang == null) {
+ xmlLang = defaultLanguage;
+ }
+
+ String subject = parseContent(parser);
+
+ if (message.getSubject(xmlLang) == null) {
+ message.addSubject(xmlLang, subject);
+ }
+ }
+ else if (elementName.equals("body")) {
+ String xmlLang = getLanguageAttribute(parser);
+ if (xmlLang == null) {
+ xmlLang = defaultLanguage;
+ }
+
+ String body = parseContent(parser);
+
+ if (message.getBody(xmlLang) == null) {
+ message.addBody(xmlLang, body);
+ }
+ }
+ 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.setThread(thread);
+ // Set packet properties.
+ if (properties != null) {
+ for (String name : properties.keySet()) {
+ message.setProperty(name, properties.get(name));
+ }
+ }
+ return message;
+ }
+
+ /**
+ * Returns the content of a tag as string regardless of any tags included.
+ *
+ * @param parser the XML pull parser
+ * @return the content of a tag as string
+ * @throws XmlPullParserException if parser encounters invalid XML
+ * @throws IOException if an IO error occurs
+ */
+ private static String parseContent(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ StringBuffer content = new StringBuffer();
+ int parserDepth = parser.getDepth();
+ while (!(parser.next() == XmlPullParser.END_TAG && parser
+ .getDepth() == parserDepth)) {
+ content.append(parser.getText());
+ }
+ return content.toString();
+ }
+
+ /**
+ * 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.available;
+ String typeString = parser.getAttributeValue("", "type");
+ if (typeString != null && !typeString.equals("")) {
+ try {
+ type = Presence.Type.valueOf(typeString);
+ }
+ catch (IllegalArgumentException iae) {
+ System.err.println("Found invalid presence type " + typeString);
+ }
+ }
+ 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);
+
+ String language = getLanguageAttribute(parser);
+ if (language != null && !"".equals(language.trim())) {
+ presence.setLanguage(language);
+ }
+ 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) {
+ // Ignore.
+ }
+ catch (IllegalArgumentException iae) {
+ // Presence priority is out of range so assume priority to be zero
+ presence.setPriority(0);
+ }
+ }
+ else if (elementName.equals("show")) {
+ String modeText = parser.nextText();
+ try {
+ presence.setMode(Presence.Mode.valueOf(modeText));
+ }
+ catch (IllegalArgumentException iae) {
+ System.err.println("Found invalid presence mode " + modeText);
+ }
+ }
+ else if (elementName.equals("error")) {
+ presence.setError(parseError(parser));
+ }
+ else if (elementName.equals("properties") &&
+ namespace.equals(PROPERTIES_NAMESPACE))
+ {
+ Map<String,Object> properties = parseProperties(parser);
+ // Set packet properties.
+ for (String name : properties.keySet()) {
+ presence.setProperty(name, properties.get(name));
+ }
+ }
+ // Otherwise, it must be a packet extension.
+ else {
+ try {
+ presence.addExtension(PacketParserUtils.parsePacketExtension(elementName, namespace, parser));
+ }
+ catch (Exception e) {
+ System.err.println("Failed to parse extension packet in Presence packet.");
+ }
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("presence")) {
+ done = true;
+ }
+ }
+ }
+ return presence;
+ }
+
+ /**
+ * 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.
+ */
+ public static IQ parseIQ(XmlPullParser parser, Connection connection) 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.getInstance().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);
+ }
+ }
+ // Only handle unknown IQs of type result. Types of 'get' and 'set' which are not understood
+ // have to be answered with an IQ error response. See the code a few lines below
+ else if (IQ.Type.RESULT == type){
+ // No Provider found for the IQ stanza, parse it to an UnparsedIQ instance
+ // so that the content of the IQ can be examined later on
+ iqPacket = new UnparsedResultIQ(parseContent(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() {
+ @Override
+ public String getChildElementXML() {
+ return null;
+ }
+ };
+ iqPacket.setPacketID(id);
+ iqPacket.setTo(from);
+ iqPacket.setFrom(to);
+ iqPacket.setType(IQ.Type.ERROR);
+ iqPacket.setError(new XMPPError(XMPPError.Condition.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() {
+ @Override
+ 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 static 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 static RosterPacket parseRoster(XmlPullParser parser) throws Exception {
+ RosterPacket roster = new RosterPacket();
+ boolean done = false;
+ RosterPacket.Item item = null;
+ while (!done) {
+ if(parser.getEventType()==XmlPullParser.START_TAG &&
+ parser.getName().equals("query")){
+ String version = parser.getAttributeValue(null, "ver");
+ roster.setVersion(version);
+ }
+ 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.valueOf(subscription != null ? subscription : "none");
+ item.setItemType(type);
+ }
+ if (parser.getName().equals("group") && item!= null) {
+ final String groupName = parser.nextText();
+ if (groupName != null && groupName.trim().length() > 0) {
+ item.addGroupName(groupName);
+ }
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("item")) {
+ roster.addRosterItem(item);
+ }
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+ }
+ return roster;
+ }
+
+ private static Registration parseRegistration(XmlPullParser parser) throws Exception {
+ Registration registration = new Registration();
+ Map<String, String> 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<String, String>();
+ }
+
+ 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 static 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;
+ }
+
+ /**
+ * Parse the available SASL mechanisms reported from the server.
+ *
+ * @param parser the XML parser, positioned at the start of the mechanisms stanza.
+ * @return a collection of Stings with the mechanisms included in the mechanisms stanza.
+ * @throws Exception if an exception occurs while parsing the stanza.
+ */
+ public static Collection<String> parseMechanisms(XmlPullParser parser) throws Exception {
+ List<String> mechanisms = new ArrayList<String>();
+ 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;
+ }
+
+ /**
+ * Parse the available compression methods reported from the server.
+ *
+ * @param parser the XML parser, positioned at the start of the compression stanza.
+ * @return a collection of Stings with the methods included in the compression stanza.
+ * @throws Exception if an exception occurs while parsing the stanza.
+ */
+ public static Collection<String> parseCompressionMethods(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ List<String> methods = new ArrayList<String>();
+ 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;
+ }
+
+ /**
+ * 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<String, Object> parseProperties(XmlPullParser parser) throws Exception {
+ Map<String, Object> properties = new HashMap<String, Object>();
+ 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 = Integer.valueOf(valueText);
+ }
+ else if ("long".equals(type)) {
+ value = Long.valueOf(valueText);
+ }
+ else if ("float".equals(type)) {
+ value = Float.valueOf(valueText);
+ }
+ else if ("double".equals(type)) {
+ value = Double.valueOf(valueText);
+ }
+ else if ("boolean".equals(type)) {
+ value = Boolean.valueOf(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 SASL authentication error packets.
+ *
+ * @param parser the XML parser.
+ * @return a SASL Failure packet.
+ * @throws Exception if an exception occurs while parsing the packet.
+ */
+ public static Failure parseSASLFailure(XmlPullParser parser) throws Exception {
+ String condition = null;
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ if (!parser.getName().equals("failure")) {
+ condition = parser.getName();
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("failure")) {
+ done = true;
+ }
+ }
+ }
+ return new Failure(condition);
+ }
+
+ /**
+ * Parses stream error packets.
+ *
+ * @param parser the XML parser.
+ * @return an stream error packet.
+ * @throws Exception if an exception occurs while parsing the packet.
+ */
+ public static 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;
+}
+
+ /**
+ * 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 {
+ final String errorNamespace = "urn:ietf:params:xml:ns:xmpp-stanzas";
+ String errorCode = "-1";
+ String type = null;
+ String message = null;
+ String condition = null;
+ List<PacketExtension> extensions = new ArrayList<PacketExtension>();
+
+ // Parse the error header
+ for (int i=0; i<parser.getAttributeCount(); i++) {
+ if (parser.getAttributeName(i).equals("code")) {
+ errorCode = parser.getAttributeValue("", "code");
+ }
+ if (parser.getAttributeName(i).equals("type")) {
+ type = parser.getAttributeValue("", "type");
+ }
+ }
+ boolean done = false;
+ // Parse the text and condition tags
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("text")) {
+ message = parser.nextText();
+ }
+ else {
+ // Condition tag, it can be xmpp error or an application defined error.
+ String elementName = parser.getName();
+ String namespace = parser.getNamespace();
+ if (errorNamespace.equals(namespace)) {
+ condition = elementName;
+ }
+ else {
+ extensions.add(parsePacketExtension(elementName, namespace, parser));
+ }
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("error")) {
+ done = true;
+ }
+ }
+ }
+ // Parse the error type.
+ XMPPError.Type errorType = XMPPError.Type.CANCEL;
+ try {
+ if (type != null) {
+ errorType = XMPPError.Type.valueOf(type.toUpperCase());
+ }
+ }
+ catch (IllegalArgumentException iae) {
+ // Print stack trace. We shouldn't be getting an illegal error type.
+ iae.printStackTrace();
+ }
+ return new XMPPError(Integer.parseInt(errorCode), errorType, condition, message, extensions);
+ }
+
+ /**
+ * 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.getInstance().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;
+ }
+
+ private static String getLanguageAttribute(XmlPullParser parser) {
+ for (int i = 0; i < parser.getAttributeCount(); i++) {
+ String attributeName = parser.getAttributeName(i);
+ if ( "xml:lang".equals(attributeName) ||
+ ("lang".equals(attributeName) &&
+ "xml".equals(parser.getAttributePrefix(i)))) {
+ return parser.getAttributeValue(i);
+ }
+ }
+ return null;
+ }
+
+ 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();
+ Class propertyType = object.getClass().getMethod(
+ "get" + Character.toUpperCase(name.charAt(0)) + name.substring(1)).getReturnType();
+ // 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.
+ object.getClass().getMethod("set" + Character.toUpperCase(name.charAt(0)) + name.substring(1), propertyType)
+ .invoke(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.
+ * @throws Exception If decoding failed due to an error.
+ */
+ 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;
+ }
+
+ /**
+ * This class represents and unparsed IQ of the type 'result'. Usually it's created when no IQProvider
+ * was found for the IQ element.
+ *
+ * The child elements can be examined with the getChildElementXML() method.
+ *
+ */
+ public static class UnparsedResultIQ extends IQ {
+ public UnparsedResultIQ(String content) {
+ this.str = content;
+ }
+
+ private final String str;
+
+ @Override
+ public String getChildElementXML() {
+ return this.str;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/util/PacketParserUtils.java.orig b/src/org/jivesoftware/smack/util/PacketParserUtils.java.orig
new file mode 100644
index 0000000..1c518f6
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/PacketParserUtils.java.orig
@@ -0,0 +1,926 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.Authentication;
+import org.jivesoftware.smack.packet.Bind;
+import org.jivesoftware.smack.packet.DefaultPacketExtension;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.Registration;
+import org.jivesoftware.smack.packet.RosterPacket;
+import org.jivesoftware.smack.packet.StreamError;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smack.provider.ProviderManager;
+import org.jivesoftware.smack.sasl.SASLMechanism.Failure;
+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")));
+ String language = getLanguageAttribute(parser);
+
+ // determine message's default language
+ String defaultLanguage = null;
+ if (language != null && !"".equals(language.trim())) {
+ message.setLanguage(language);
+ defaultLanguage = language;
+ }
+ else {
+ defaultLanguage = Packet.getDefaultLanguage();
+ }
+
+ // 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 thread = null;
+ Map<String, Object> 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")) {
+ String xmlLang = getLanguageAttribute(parser);
+ if (xmlLang == null) {
+ xmlLang = defaultLanguage;
+ }
+
+ String subject = parseContent(parser);
+
+ if (message.getSubject(xmlLang) == null) {
+ message.addSubject(xmlLang, subject);
+ }
+ }
+ else if (elementName.equals("body")) {
+ String xmlLang = getLanguageAttribute(parser);
+ if (xmlLang == null) {
+ xmlLang = defaultLanguage;
+ }
+
+ String body = parseContent(parser);
+
+ if (message.getBody(xmlLang) == null) {
+ message.addBody(xmlLang, body);
+ }
+ }
+ 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.setThread(thread);
+ // Set packet properties.
+ if (properties != null) {
+ for (String name : properties.keySet()) {
+ message.setProperty(name, properties.get(name));
+ }
+ }
+ return message;
+ }
+
+ /**
+ * Returns the content of a tag as string regardless of any tags included.
+ *
+ * @param parser the XML pull parser
+ * @return the content of a tag as string
+ * @throws XmlPullParserException if parser encounters invalid XML
+ * @throws IOException if an IO error occurs
+ */
+ private static String parseContent(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ StringBuffer content = new StringBuffer();
+ int parserDepth = parser.getDepth();
+ while (!(parser.next() == XmlPullParser.END_TAG && parser
+ .getDepth() == parserDepth)) {
+ content.append(parser.getText());
+ }
+ return content.toString();
+ }
+
+ /**
+ * 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.available;
+ String typeString = parser.getAttributeValue("", "type");
+ if (typeString != null && !typeString.equals("")) {
+ try {
+ type = Presence.Type.valueOf(typeString);
+ }
+ catch (IllegalArgumentException iae) {
+ System.err.println("Found invalid presence type " + typeString);
+ }
+ }
+ 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);
+
+ String language = getLanguageAttribute(parser);
+ if (language != null && !"".equals(language.trim())) {
+ presence.setLanguage(language);
+ }
+ 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) {
+ // Ignore.
+ }
+ catch (IllegalArgumentException iae) {
+ // Presence priority is out of range so assume priority to be zero
+ presence.setPriority(0);
+ }
+ }
+ else if (elementName.equals("show")) {
+ String modeText = parser.nextText();
+ try {
+ presence.setMode(Presence.Mode.valueOf(modeText));
+ }
+ catch (IllegalArgumentException iae) {
+ System.err.println("Found invalid presence mode " + modeText);
+ }
+ }
+ else if (elementName.equals("error")) {
+ presence.setError(parseError(parser));
+ }
+ else if (elementName.equals("properties") &&
+ namespace.equals(PROPERTIES_NAMESPACE))
+ {
+ Map<String,Object> properties = parseProperties(parser);
+ // Set packet properties.
+ for (String name : properties.keySet()) {
+ presence.setProperty(name, properties.get(name));
+ }
+ }
+ // Otherwise, it must be a packet extension.
+ else {
+ try {
+ presence.addExtension(PacketParserUtils.parsePacketExtension(elementName, namespace, parser));
+ }
+ catch (Exception e) {
+ System.err.println("Failed to parse extension packet in Presence packet.");
+ }
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("presence")) {
+ done = true;
+ }
+ }
+ }
+ return presence;
+ }
+
+ /**
+ * 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.
+ */
+ public static IQ parseIQ(XmlPullParser parser, Connection connection) 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.getInstance().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);
+ }
+ }
+ // Only handle unknown IQs of type result. Types of 'get' and 'set' which are not understood
+ // have to be answered with an IQ error response. See the code a few lines below
+ else if (IQ.Type.RESULT == type){
+ // No Provider found for the IQ stanza, parse it to an UnparsedIQ instance
+ // so that the content of the IQ can be examined later on
+ iqPacket = new UnparsedResultIQ(parseContent(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() {
+ @Override
+ public String getChildElementXML() {
+ return null;
+ }
+ };
+ iqPacket.setPacketID(id);
+ iqPacket.setTo(from);
+ iqPacket.setFrom(to);
+ iqPacket.setType(IQ.Type.ERROR);
+ iqPacket.setError(new XMPPError(XMPPError.Condition.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() {
+ @Override
+ 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 static 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 static RosterPacket parseRoster(XmlPullParser parser) throws Exception {
+ RosterPacket roster = new RosterPacket();
+ boolean done = false;
+ RosterPacket.Item item = null;
+ while (!done) {
+ if(parser.getEventType()==XmlPullParser.START_TAG &&
+ parser.getName().equals("query")){
+ String version = parser.getAttributeValue(null, "ver");
+ roster.setVersion(version);
+ }
+ 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.valueOf(subscription != null ? subscription : "none");
+ item.setItemType(type);
+ }
+ if (parser.getName().equals("group") && item!= null) {
+ final String groupName = parser.nextText();
+ if (groupName != null && groupName.trim().length() > 0) {
+ item.addGroupName(groupName);
+ }
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("item")) {
+ roster.addRosterItem(item);
+ }
+ if (parser.getName().equals("query")) {
+ done = true;
+ }
+ }
+ }
+ return roster;
+ }
+
+ private static Registration parseRegistration(XmlPullParser parser) throws Exception {
+ Registration registration = new Registration();
+ Map<String, String> 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<String, String>();
+ }
+
+ 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 static 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;
+ }
+
+ /**
+ * Parse the available SASL mechanisms reported from the server.
+ *
+ * @param parser the XML parser, positioned at the start of the mechanisms stanza.
+ * @return a collection of Stings with the mechanisms included in the mechanisms stanza.
+ * @throws Exception if an exception occurs while parsing the stanza.
+ */
+ public static Collection<String> parseMechanisms(XmlPullParser parser) throws Exception {
+ List<String> mechanisms = new ArrayList<String>();
+ 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;
+ }
+
+ /**
+ * Parse the available compression methods reported from the server.
+ *
+ * @param parser the XML parser, positioned at the start of the compression stanza.
+ * @return a collection of Stings with the methods included in the compression stanza.
+ * @throws Exception if an exception occurs while parsing the stanza.
+ */
+ public static Collection<String> parseCompressionMethods(XmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ List<String> methods = new ArrayList<String>();
+ 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;
+ }
+
+ /**
+ * 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<String, Object> parseProperties(XmlPullParser parser) throws Exception {
+ Map<String, Object> properties = new HashMap<String, Object>();
+ 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 = Integer.valueOf(valueText);
+ }
+ else if ("long".equals(type)) {
+ value = Long.valueOf(valueText);
+ }
+ else if ("float".equals(type)) {
+ value = Float.valueOf(valueText);
+ }
+ else if ("double".equals(type)) {
+ value = Double.valueOf(valueText);
+ }
+ else if ("boolean".equals(type)) {
+ value = Boolean.valueOf(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 SASL authentication error packets.
+ *
+ * @param parser the XML parser.
+ * @return a SASL Failure packet.
+ * @throws Exception if an exception occurs while parsing the packet.
+ */
+ public static Failure parseSASLFailure(XmlPullParser parser) throws Exception {
+ String condition = null;
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ if (!parser.getName().equals("failure")) {
+ condition = parser.getName();
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("failure")) {
+ done = true;
+ }
+ }
+ }
+ return new Failure(condition);
+ }
+
+ /**
+ * Parses stream error packets.
+ *
+ * @param parser the XML parser.
+ * @return an stream error packet.
+ * @throws Exception if an exception occurs while parsing the packet.
+ */
+ public static 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;
+}
+
+ /**
+ * 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 {
+ final String errorNamespace = "urn:ietf:params:xml:ns:xmpp-stanzas";
+ String errorCode = "-1";
+ String type = null;
+ String message = null;
+ String condition = null;
+ List<PacketExtension> extensions = new ArrayList<PacketExtension>();
+
+ // Parse the error header
+ for (int i=0; i<parser.getAttributeCount(); i++) {
+ if (parser.getAttributeName(i).equals("code")) {
+ errorCode = parser.getAttributeValue("", "code");
+ }
+ if (parser.getAttributeName(i).equals("type")) {
+ type = parser.getAttributeValue("", "type");
+ }
+ }
+ boolean done = false;
+ // Parse the text and condition tags
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("text")) {
+ message = parser.nextText();
+ }
+ else {
+ // Condition tag, it can be xmpp error or an application defined error.
+ String elementName = parser.getName();
+ String namespace = parser.getNamespace();
+ if (errorNamespace.equals(namespace)) {
+ condition = elementName;
+ }
+ else {
+ extensions.add(parsePacketExtension(elementName, namespace, parser));
+ }
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("error")) {
+ done = true;
+ }
+ }
+ }
+ // Parse the error type.
+ XMPPError.Type errorType = XMPPError.Type.CANCEL;
+ try {
+ if (type != null) {
+ errorType = XMPPError.Type.valueOf(type.toUpperCase());
+ }
+ }
+ catch (IllegalArgumentException iae) {
+ // Print stack trace. We shouldn't be getting an illegal error type.
+ iae.printStackTrace();
+ }
+ return new XMPPError(Integer.parseInt(errorCode), errorType, condition, message, extensions);
+ }
+
+ /**
+ * 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.getInstance().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;
+ }
+
+ private static String getLanguageAttribute(XmlPullParser parser) {
+ for (int i = 0; i < parser.getAttributeCount(); i++) {
+ String attributeName = parser.getAttributeName(i);
+ if ( "xml:lang".equals(attributeName) ||
+ ("lang".equals(attributeName) &&
+ "xml".equals(parser.getAttributePrefix(i)))) {
+ return parser.getAttributeValue(i);
+ }
+ }
+ return null;
+ }
+
+ 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, 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.
+ * @throws Exception If decoding failed due to an error.
+ */
+ 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;
+ }
+
+ /**
+ * This class represents and unparsed IQ of the type 'result'. Usually it's created when no IQProvider
+ * was found for the IQ element.
+ *
+ * The child elements can be examined with the getChildElementXML() method.
+ *
+ */
+ public static class UnparsedResultIQ extends IQ {
+ public UnparsedResultIQ(String content) {
+ this.str = content;
+ }
+
+ private final String str;
+
+ @Override
+ public String getChildElementXML() {
+ return this.str;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smack/util/ReaderListener.java b/src/org/jivesoftware/smack/util/ReaderListener.java
new file mode 100644
index 0000000..9f1f5bb
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/ReaderListener.java
@@ -0,0 +1,41 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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/src/org/jivesoftware/smack/util/StringEncoder.java b/src/org/jivesoftware/smack/util/StringEncoder.java
new file mode 100644
index 0000000..4c3d373
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/StringEncoder.java
@@ -0,0 +1,36 @@
+/**
+ * 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.
+ */
+
+/**
+ * @author Florian Schmaus
+ */
+package org.jivesoftware.smack.util;
+
+public interface StringEncoder {
+ /**
+ * Encodes an string to another representation
+ *
+ * @param string
+ * @return
+ */
+ String encode(String string);
+
+ /**
+ * Decodes an string back to it's initial representation
+ *
+ * @param string
+ * @return
+ */
+ String decode(String string);
+}
diff --git a/src/org/jivesoftware/smack/util/StringUtils.java b/src/org/jivesoftware/smack/util/StringUtils.java
new file mode 100644
index 0000000..7e3cfdc
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/StringUtils.java
@@ -0,0 +1,800 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A collection of utility methods for String objects.
+ */
+public class StringUtils {
+
+ /**
+ * Date format as defined in XEP-0082 - XMPP Date and Time Profiles. The time zone is set to
+ * UTC.
+ * <p>
+ * Date formats are not synchronized. Since multiple threads access the format concurrently, it
+ * must be synchronized externally or you can use the convenience methods
+ * {@link #parseXEP0082Date(String)} and {@link #formatXEP0082Date(Date)}.
+ * @deprecated This public version will be removed in favor of using the methods defined within this class.
+ */
+ public static final DateFormat XEP_0082_UTC_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+
+ /*
+ * private version to use internally so we don't have to be concerned with thread safety.
+ */
+ private static final DateFormat dateFormatter = DateFormatType.XEP_0082_DATE_PROFILE.createFormatter();
+ private static final Pattern datePattern = Pattern.compile("^\\d+-\\d+-\\d+$");
+
+ private static final DateFormat timeFormatter = DateFormatType.XEP_0082_TIME_MILLIS_ZONE_PROFILE.createFormatter();
+ private static final Pattern timePattern = Pattern.compile("^(\\d+:){2}\\d+.\\d+(Z|([+-](\\d+:\\d+)))$");
+ private static final DateFormat timeNoZoneFormatter = DateFormatType.XEP_0082_TIME_MILLIS_PROFILE.createFormatter();
+ private static final Pattern timeNoZonePattern = Pattern.compile("^(\\d+:){2}\\d+.\\d+$");
+
+ private static final DateFormat timeNoMillisFormatter = DateFormatType.XEP_0082_TIME_ZONE_PROFILE.createFormatter();
+ private static final Pattern timeNoMillisPattern = Pattern.compile("^(\\d+:){2}\\d+(Z|([+-](\\d+:\\d+)))$");
+ private static final DateFormat timeNoMillisNoZoneFormatter = DateFormatType.XEP_0082_TIME_PROFILE.createFormatter();
+ private static final Pattern timeNoMillisNoZonePattern = Pattern.compile("^(\\d+:){2}\\d+$");
+
+ private static final DateFormat dateTimeFormatter = DateFormatType.XEP_0082_DATETIME_MILLIS_PROFILE.createFormatter();
+ private static final Pattern dateTimePattern = Pattern.compile("^\\d+(-\\d+){2}+T(\\d+:){2}\\d+.\\d+(Z|([+-](\\d+:\\d+)))?$");
+ private static final DateFormat dateTimeNoMillisFormatter = DateFormatType.XEP_0082_DATETIME_PROFILE.createFormatter();
+ private static final Pattern dateTimeNoMillisPattern = Pattern.compile("^\\d+(-\\d+){2}+T(\\d+:){2}\\d+(Z|([+-](\\d+:\\d+)))?$");
+
+ private static final DateFormat xep0091Formatter = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
+ private static final DateFormat xep0091Date6DigitFormatter = new SimpleDateFormat("yyyyMd'T'HH:mm:ss");
+ private static final DateFormat xep0091Date7Digit1MonthFormatter = new SimpleDateFormat("yyyyMdd'T'HH:mm:ss");
+ private static final DateFormat xep0091Date7Digit2MonthFormatter = new SimpleDateFormat("yyyyMMd'T'HH:mm:ss");
+ private static final Pattern xep0091Pattern = Pattern.compile("^\\d+T\\d+:\\d+:\\d+$");
+
+ private static final List<PatternCouplings> couplings = new ArrayList<PatternCouplings>();
+
+ static {
+ TimeZone utc = TimeZone.getTimeZone("UTC");
+ XEP_0082_UTC_FORMAT.setTimeZone(utc);
+ dateFormatter.setTimeZone(utc);
+ timeFormatter.setTimeZone(utc);
+ timeNoZoneFormatter.setTimeZone(utc);
+ timeNoMillisFormatter.setTimeZone(utc);
+ timeNoMillisNoZoneFormatter.setTimeZone(utc);
+ dateTimeFormatter.setTimeZone(utc);
+ dateTimeNoMillisFormatter.setTimeZone(utc);
+
+ xep0091Formatter.setTimeZone(utc);
+ xep0091Date6DigitFormatter.setTimeZone(utc);
+ xep0091Date7Digit1MonthFormatter.setTimeZone(utc);
+ xep0091Date7Digit1MonthFormatter.setLenient(false);
+ xep0091Date7Digit2MonthFormatter.setTimeZone(utc);
+ xep0091Date7Digit2MonthFormatter.setLenient(false);
+
+ couplings.add(new PatternCouplings(datePattern, dateFormatter));
+ couplings.add(new PatternCouplings(dateTimePattern, dateTimeFormatter, true));
+ couplings.add(new PatternCouplings(dateTimeNoMillisPattern, dateTimeNoMillisFormatter, true));
+ couplings.add(new PatternCouplings(timePattern, timeFormatter, true));
+ couplings.add(new PatternCouplings(timeNoZonePattern, timeNoZoneFormatter));
+ couplings.add(new PatternCouplings(timeNoMillisPattern, timeNoMillisFormatter, true));
+ couplings.add(new PatternCouplings(timeNoMillisNoZonePattern, timeNoMillisNoZoneFormatter));
+ }
+
+ private static final char[] QUOTE_ENCODE = "&quot;".toCharArray();
+ private static final char[] APOS_ENCODE = "&apos;".toCharArray();
+ private static final char[] AMP_ENCODE = "&amp;".toCharArray();
+ private static final char[] LT_ENCODE = "&lt;".toCharArray();
+ private static final char[] GT_ENCODE = "&gt;".toCharArray();
+
+ /**
+ * Parses the given date string in the <a href="http://xmpp.org/extensions/xep-0082.html">XEP-0082 - XMPP Date and Time Profiles</a>.
+ *
+ * @param dateString the date string to parse
+ * @return the parsed Date
+ * @throws ParseException if the specified string cannot be parsed
+ * @deprecated Use {@link #parseDate(String)} instead.
+ *
+ */
+ public static Date parseXEP0082Date(String dateString) throws ParseException {
+ return parseDate(dateString);
+ }
+
+ /**
+ * Parses the given date string in either of the three profiles of <a href="http://xmpp.org/extensions/xep-0082.html">XEP-0082 - XMPP Date and Time Profiles</a>
+ * or <a href="http://xmpp.org/extensions/xep-0091.html">XEP-0091 - Legacy Delayed Delivery</a> format.
+ * <p>
+ * This method uses internal date formatters and is thus threadsafe.
+ * @param dateString the date string to parse
+ * @return the parsed Date
+ * @throws ParseException if the specified string cannot be parsed
+ */
+ public static Date parseDate(String dateString) throws ParseException {
+ Matcher matcher = xep0091Pattern.matcher(dateString);
+
+ /*
+ * if date is in XEP-0091 format handle ambiguous dates missing the
+ * leading zero in month and day
+ */
+ if (matcher.matches()) {
+ int length = dateString.split("T")[0].length();
+
+ if (length < 8) {
+ Date date = handleDateWithMissingLeadingZeros(dateString, length);
+
+ if (date != null)
+ return date;
+ }
+ else {
+ synchronized (xep0091Formatter) {
+ return xep0091Formatter.parse(dateString);
+ }
+ }
+ }
+ else {
+ for (PatternCouplings coupling : couplings) {
+ matcher = coupling.pattern.matcher(dateString);
+
+ if (matcher.matches())
+ {
+ if (coupling.needToConvertTimeZone) {
+ dateString = coupling.convertTime(dateString);
+ }
+
+ synchronized (coupling.formatter) {
+ return coupling.formatter.parse(dateString);
+ }
+ }
+ }
+ }
+
+ /*
+ * We assume it is the XEP-0082 DateTime profile with no milliseconds at this point. If it isn't, is is just not parseable, then we attempt
+ * to parse it regardless and let it throw the ParseException.
+ */
+ synchronized (dateTimeNoMillisFormatter) {
+ return dateTimeNoMillisFormatter.parse(dateString);
+ }
+ }
+
+ /**
+ * Parses the given date string in different ways and returns the date that
+ * lies in the past and/or is nearest to the current date-time.
+ *
+ * @param stampString date in string representation
+ * @param dateLength
+ * @param noFuture
+ * @return the parsed date
+ * @throws ParseException The date string was of an unknown format
+ */
+ private static Date handleDateWithMissingLeadingZeros(String stampString, int dateLength) throws ParseException {
+ if (dateLength == 6) {
+ synchronized (xep0091Date6DigitFormatter) {
+ return xep0091Date6DigitFormatter.parse(stampString);
+ }
+ }
+ Calendar now = Calendar.getInstance();
+
+ Calendar oneDigitMonth = parseXEP91Date(stampString, xep0091Date7Digit1MonthFormatter);
+ Calendar twoDigitMonth = parseXEP91Date(stampString, xep0091Date7Digit2MonthFormatter);
+
+ List<Calendar> dates = filterDatesBefore(now, oneDigitMonth, twoDigitMonth);
+
+ if (!dates.isEmpty()) {
+ return determineNearestDate(now, dates).getTime();
+ }
+ return null;
+ }
+
+ private static Calendar parseXEP91Date(String stampString, DateFormat dateFormat) {
+ try {
+ synchronized (dateFormat) {
+ dateFormat.parse(stampString);
+ return dateFormat.getCalendar();
+ }
+ }
+ catch (ParseException e) {
+ return null;
+ }
+ }
+
+ private static List<Calendar> filterDatesBefore(Calendar now, Calendar... dates) {
+ List<Calendar> result = new ArrayList<Calendar>();
+
+ for (Calendar calendar : dates) {
+ if (calendar != null && calendar.before(now)) {
+ result.add(calendar);
+ }
+ }
+
+ return result;
+ }
+
+ private static Calendar determineNearestDate(final Calendar now, List<Calendar> dates) {
+
+ Collections.sort(dates, new Comparator<Calendar>() {
+
+ public int compare(Calendar o1, Calendar o2) {
+ Long diff1 = new Long(now.getTimeInMillis() - o1.getTimeInMillis());
+ Long diff2 = new Long(now.getTimeInMillis() - o2.getTimeInMillis());
+ return diff1.compareTo(diff2);
+ }
+
+ });
+
+ return dates.get(0);
+ }
+
+ /**
+ * Formats a Date into a XEP-0082 - XMPP Date and Time Profiles string.
+ *
+ * @param date the time value to be formatted into a time string
+ * @return the formatted time string in XEP-0082 format
+ */
+ public static String formatXEP0082Date(Date date) {
+ synchronized (dateTimeFormatter) {
+ return dateTimeFormatter.format(date);
+ }
+ }
+
+ public static String formatDate(Date toFormat, DateFormatType type)
+ {
+ return null;
+ }
+
+ /**
+ * 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.lastIndexOf("@");
+ 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.lastIndexOf("@");
+ // If the String ends with '@', return the empty string.
+ if (atIndex + 1 > XMPPAddress.length()) {
+ return "";
+ }
+ int slashIndex = XMPPAddress.indexOf("/");
+ if (slashIndex > 0 && slashIndex > atIndex) {
+ 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);
+ }
+ }
+
+ /**
+ * Returns true if jid is a full JID (i.e. a JID with resource part).
+ *
+ * @param jid
+ * @return true if full JID, false otherwise
+ */
+ public static boolean isFullJID(String jid) {
+ if (parseName(jid).length() <= 0 || parseServer(jid).length() <= 0
+ || parseResource(jid).length() <= 0) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Escapes the node portion of a JID according to "JID Escaping" (JEP-0106).
+ * Escaping replaces characters prohibited by node-prep with escape sequences,
+ * as follows:<p>
+ *
+ * <table border="1">
+ * <tr><td><b>Unescaped Character</b></td><td><b>Encoded Sequence</b></td></tr>
+ * <tr><td>&lt;space&gt;</td><td>\20</td></tr>
+ * <tr><td>"</td><td>\22</td></tr>
+ * <tr><td>&</td><td>\26</td></tr>
+ * <tr><td>'</td><td>\27</td></tr>
+ * <tr><td>/</td><td>\2f</td></tr>
+ * <tr><td>:</td><td>\3a</td></tr>
+ * <tr><td>&lt;</td><td>\3c</td></tr>
+ * <tr><td>&gt;</td><td>\3e</td></tr>
+ * <tr><td>@</td><td>\40</td></tr>
+ * <tr><td>\</td><td>\5c</td></tr>
+ * </table><p>
+ *
+ * This process is useful when the node comes from an external source that doesn't
+ * conform to nodeprep. For example, a username in LDAP may be "Joe Smith". Because
+ * the &lt;space&gt; character isn't a valid part of a node, the username should
+ * be escaped to "Joe\20Smith" before being made into a JID (e.g. "joe\20smith@example.com"
+ * after case-folding, etc. has been applied).<p>
+ *
+ * All node escaping and un-escaping must be performed manually at the appropriate
+ * time; the JID class will not escape or un-escape automatically.
+ *
+ * @param node the node.
+ * @return the escaped version of the node.
+ */
+ public static String escapeNode(String node) {
+ if (node == null) {
+ return null;
+ }
+ StringBuilder buf = new StringBuilder(node.length() + 8);
+ for (int i=0, n=node.length(); i<n; i++) {
+ char c = node.charAt(i);
+ switch (c) {
+ case '"': buf.append("\\22"); break;
+ case '&': buf.append("\\26"); break;
+ case '\'': buf.append("\\27"); break;
+ case '/': buf.append("\\2f"); break;
+ case ':': buf.append("\\3a"); break;
+ case '<': buf.append("\\3c"); break;
+ case '>': buf.append("\\3e"); break;
+ case '@': buf.append("\\40"); break;
+ case '\\': buf.append("\\5c"); break;
+ default: {
+ if (Character.isWhitespace(c)) {
+ buf.append("\\20");
+ }
+ else {
+ buf.append(c);
+ }
+ }
+ }
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Un-escapes the node portion of a JID according to "JID Escaping" (JEP-0106).<p>
+ * Escaping replaces characters prohibited by node-prep with escape sequences,
+ * as follows:<p>
+ *
+ * <table border="1">
+ * <tr><td><b>Unescaped Character</b></td><td><b>Encoded Sequence</b></td></tr>
+ * <tr><td>&lt;space&gt;</td><td>\20</td></tr>
+ * <tr><td>"</td><td>\22</td></tr>
+ * <tr><td>&</td><td>\26</td></tr>
+ * <tr><td>'</td><td>\27</td></tr>
+ * <tr><td>/</td><td>\2f</td></tr>
+ * <tr><td>:</td><td>\3a</td></tr>
+ * <tr><td>&lt;</td><td>\3c</td></tr>
+ * <tr><td>&gt;</td><td>\3e</td></tr>
+ * <tr><td>@</td><td>\40</td></tr>
+ * <tr><td>\</td><td>\5c</td></tr>
+ * </table><p>
+ *
+ * This process is useful when the node comes from an external source that doesn't
+ * conform to nodeprep. For example, a username in LDAP may be "Joe Smith". Because
+ * the &lt;space&gt; character isn't a valid part of a node, the username should
+ * be escaped to "Joe\20Smith" before being made into a JID (e.g. "joe\20smith@example.com"
+ * after case-folding, etc. has been applied).<p>
+ *
+ * All node escaping and un-escaping must be performed manually at the appropriate
+ * time; the JID class will not escape or un-escape automatically.
+ *
+ * @param node the escaped version of the node.
+ * @return the un-escaped version of the node.
+ */
+ public static String unescapeNode(String node) {
+ if (node == null) {
+ return null;
+ }
+ char [] nodeChars = node.toCharArray();
+ StringBuilder buf = new StringBuilder(nodeChars.length);
+ for (int i=0, n=nodeChars.length; i<n; i++) {
+ compare: {
+ char c = node.charAt(i);
+ if (c == '\\' && i+2<n) {
+ char c2 = nodeChars[i+1];
+ char c3 = nodeChars[i+2];
+ if (c2 == '2') {
+ switch (c3) {
+ case '0': buf.append(' '); i+=2; break compare;
+ case '2': buf.append('"'); i+=2; break compare;
+ case '6': buf.append('&'); i+=2; break compare;
+ case '7': buf.append('\''); i+=2; break compare;
+ case 'f': buf.append('/'); i+=2; break compare;
+ }
+ }
+ else if (c2 == '3') {
+ switch (c3) {
+ case 'a': buf.append(':'); i+=2; break compare;
+ case 'c': buf.append('<'); i+=2; break compare;
+ case 'e': buf.append('>'); i+=2; break compare;
+ }
+ }
+ else if (c2 == '4') {
+ if (c3 == '0') {
+ buf.append("@");
+ i+=2;
+ break compare;
+ }
+ }
+ else if (c2 == '5') {
+ if (c3 == 'c') {
+ buf.append("\\");
+ i+=2;
+ break compare;
+ }
+ }
+ }
+ buf.append(c);
+ }
+ }
+ return buf.toString();
+ }
+
+ /**
+ * 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;
+ StringBuilder out = new StringBuilder((int)(len*1.3));
+ for (; i < len; i++) {
+ ch = input[i];
+ if (ch > '>') {
+ }
+ 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 &#235; (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);
+ }
+ else if (ch == '\'') {
+ if (i > last) {
+ out.append(input, last, i - last);
+ }
+ last = i + 1;
+ out.append(APOS_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) {
+ StringBuilder hex = new StringBuilder(bytes.length * 2);
+
+ for (byte aByte : bytes) {
+ if (((int) aByte & 0xff) < 0x10) {
+ hex.append("0");
+ }
+ hex.append(Integer.toString((int) aByte & 0xff, 16));
+ }
+
+ return hex.toString();
+ }
+
+ /**
+ * 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) {
+ return encodeBase64(data, false);
+ }
+
+ /**
+ * Encodes a byte array into a bse64 String.
+ *
+ * @param data The byte arry to encode.
+ * @param lineBreaks True if the encoding should contain line breaks and false if it should not.
+ * @return A base64 encoded String.
+ */
+ public static String encodeBase64(byte[] data, boolean lineBreaks) {
+ return encodeBase64(data, 0, data.length, lineBreaks);
+ }
+
+ /**
+ * Encodes a byte array into a bse64 String.
+ *
+ * @param data The byte arry to encode.
+ * @param offset the offset of the bytearray to begin encoding at.
+ * @param len the length of bytes to encode.
+ * @param lineBreaks True if the encoding should contain line breaks and false if it should not.
+ * @return A base64 encoded String.
+ */
+ public static String encodeBase64(byte[] data, int offset, int len, boolean lineBreaks) {
+ return Base64.encodeBytes(data, offset, len, (lineBreaks ? Base64.NO_OPTIONS : Base64.DONT_BREAK_LINES));
+ }
+
+ /**
+ * Decodes a base64 String.
+ * Unlike Base64.decode() this method does not try to detect and decompress a gzip-compressed input.
+ *
+ * @param data a base64 encoded String to decode.
+ * @return the decoded String.
+ */
+ public static byte[] decodeBase64(String data) {
+ byte[] bytes;
+ try {
+ bytes = data.getBytes("UTF-8");
+ } catch (java.io.UnsupportedEncodingException uee) {
+ bytes = data.getBytes();
+ }
+
+ bytes = Base64.decode(bytes, 0, bytes.length, Base64.NO_OPTIONS);
+ return bytes;
+ }
+
+ /**
+ * 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 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.
+ }
+
+ private static class PatternCouplings {
+ Pattern pattern;
+ DateFormat formatter;
+ boolean needToConvertTimeZone = false;
+
+ public PatternCouplings(Pattern datePattern, DateFormat dateFormat) {
+ pattern = datePattern;
+ formatter = dateFormat;
+ }
+
+ public PatternCouplings(Pattern datePattern, DateFormat dateFormat, boolean shouldConvertToRFC822) {
+ pattern = datePattern;
+ formatter = dateFormat;
+ needToConvertTimeZone = shouldConvertToRFC822;
+ }
+
+ public String convertTime(String dateString) {
+ if (dateString.charAt(dateString.length() - 1) == 'Z') {
+ return dateString.replace("Z", "+0000");
+ }
+ else {
+ // If the time zone wasn't specified with 'Z', then it's in
+ // ISO8601 format (i.e. '(+|-)HH:mm')
+ // RFC822 needs a similar format just without the colon (i.e.
+ // '(+|-)HHmm)'), so remove it
+ return dateString.replaceAll("([\\+\\-]\\d\\d):(\\d\\d)","$1$2");
+ }
+ }
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/util/SyncPacketSend.java b/src/org/jivesoftware/smack/util/SyncPacketSend.java
new file mode 100644
index 0000000..a1c238a
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/SyncPacketSend.java
@@ -0,0 +1,63 @@
+/**
+ * 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 org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Utility class for doing synchronous calls to the server. Provides several
+ * methods for sending a packet to the server and waiting for the reply.
+ *
+ * @author Robin Collier
+ */
+final public class SyncPacketSend
+{
+ private SyncPacketSend()
+ { }
+
+ static public Packet getReply(Connection connection, Packet packet, long timeout)
+ throws XMPPException
+ {
+ PacketFilter responseFilter = new PacketIDFilter(packet.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+
+ connection.sendPacket(packet);
+
+ // Wait up to a certain number of seconds for a reply.
+ Packet result = response.nextResult(timeout);
+
+ // Stop queuing results
+ response.cancel();
+
+ if (result == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (result.getError() != null) {
+ throw new XMPPException(result.getError());
+ }
+ return result;
+ }
+
+ static public Packet getReply(Connection connection, Packet packet)
+ throws XMPPException
+ {
+ return getReply(connection, packet, SmackConfiguration.getPacketReplyTimeout());
+ }
+}
diff --git a/src/org/jivesoftware/smack/util/WriterListener.java b/src/org/jivesoftware/smack/util/WriterListener.java
new file mode 100644
index 0000000..dcf56d9
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/WriterListener.java
@@ -0,0 +1,41 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 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/src/org/jivesoftware/smack/util/collections/AbstractEmptyIterator.java b/src/org/jivesoftware/smack/util/collections/AbstractEmptyIterator.java
new file mode 100644
index 0000000..c2ec156
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/collections/AbstractEmptyIterator.java
@@ -0,0 +1,89 @@
+// GenericsNote: Converted.
+/*
+ * Copyright 2004 The Apache Software Foundation
+ *
+ * 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.collections;
+
+import java.util.NoSuchElementException;
+
+/**
+ * Provides an implementation of an empty iterator.
+ *
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:24 $
+ * @since Commons Collections 3.1
+ */
+abstract class AbstractEmptyIterator <E> {
+
+ /**
+ * Constructor.
+ */
+ protected AbstractEmptyIterator() {
+ super();
+ }
+
+ public boolean hasNext() {
+ return false;
+ }
+
+ public E next() {
+ throw new NoSuchElementException("Iterator contains no elements");
+ }
+
+ public boolean hasPrevious() {
+ return false;
+ }
+
+ public E previous() {
+ throw new NoSuchElementException("Iterator contains no elements");
+ }
+
+ public int nextIndex() {
+ return 0;
+ }
+
+ public int previousIndex() {
+ return -1;
+ }
+
+ public void add(E obj) {
+ throw new UnsupportedOperationException("add() not supported for empty Iterator");
+ }
+
+ public void set(E obj) {
+ throw new IllegalStateException("Iterator contains no elements");
+ }
+
+ public void remove() {
+ throw new IllegalStateException("Iterator contains no elements");
+ }
+
+ public E getKey() {
+ throw new IllegalStateException("Iterator contains no elements");
+ }
+
+ public E getValue() {
+ throw new IllegalStateException("Iterator contains no elements");
+ }
+
+ public E setValue(E value) {
+ throw new IllegalStateException("Iterator contains no elements");
+ }
+
+ public void reset() {
+ // do nothing
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/util/collections/AbstractHashedMap.java b/src/org/jivesoftware/smack/util/collections/AbstractHashedMap.java
new file mode 100644
index 0000000..f6fb34a
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/collections/AbstractHashedMap.java
@@ -0,0 +1,1338 @@
+// GenericsNote: Converted -- However, null keys will now be represented in the internal structures, a big change.
+/*
+ * Copyright 2003-2004 The Apache Software Foundation
+ *
+ * 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.collections;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.*;
+
+/**
+ * An abstract implementation of a hash-based map which provides numerous points for
+ * subclasses to override.
+ * <p/>
+ * This class implements all the features necessary for a subclass hash-based map.
+ * Key-value entries are stored in instances of the <code>HashEntry</code> class,
+ * which can be overridden and replaced. The iterators can similarly be replaced,
+ * without the need to replace the KeySet, EntrySet and Values view classes.
+ * <p/>
+ * Overridable methods are provided to change the default hashing behaviour, and
+ * to change how entries are added to and removed from the map. Hopefully, all you
+ * need for unusual subclasses is here.
+ * <p/>
+ * NOTE: From Commons Collections 3.1 this class extends AbstractMap.
+ * This is to provide backwards compatibility for ReferenceMap between v3.0 and v3.1.
+ * This extends clause will be removed in v4.0.
+ *
+ * @author java util HashMap
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $
+ * @since Commons Collections 3.0
+ */
+public class AbstractHashedMap <K,V> extends AbstractMap<K, V> implements IterableMap<K, V> {
+
+ protected static final String NO_NEXT_ENTRY = "No next() entry in the iteration";
+ protected static final String NO_PREVIOUS_ENTRY = "No previous() entry in the iteration";
+ protected static final String REMOVE_INVALID = "remove() can only be called once after next()";
+ protected static final String GETKEY_INVALID = "getKey() can only be called after next() and before remove()";
+ protected static final String GETVALUE_INVALID = "getValue() can only be called after next() and before remove()";
+ protected static final String SETVALUE_INVALID = "setValue() can only be called after next() and before remove()";
+
+ /**
+ * The default capacity to use
+ */
+ protected static final int DEFAULT_CAPACITY = 16;
+ /**
+ * The default threshold to use
+ */
+ protected static final int DEFAULT_THRESHOLD = 12;
+ /**
+ * The default load factor to use
+ */
+ protected static final float DEFAULT_LOAD_FACTOR = 0.75f;
+ /**
+ * The maximum capacity allowed
+ */
+ protected static final int MAXIMUM_CAPACITY = 1 << 30;
+ /**
+ * An object for masking null
+ */
+ protected static final Object NULL = new Object();
+
+ /**
+ * Load factor, normally 0.75
+ */
+ protected transient float loadFactor;
+ /**
+ * The size of the map
+ */
+ protected transient int size;
+ /**
+ * Map entries
+ */
+ protected transient HashEntry<K, V>[] data;
+ /**
+ * Size at which to rehash
+ */
+ protected transient int threshold;
+ /**
+ * Modification count for iterators
+ */
+ protected transient int modCount;
+ /**
+ * Entry set
+ */
+ protected transient EntrySet<K, V> entrySet;
+ /**
+ * Key set
+ */
+ protected transient KeySet<K, V> keySet;
+ /**
+ * Values
+ */
+ protected transient Values<K, V> values;
+
+ /**
+ * Constructor only used in deserialization, do not use otherwise.
+ */
+ protected AbstractHashedMap() {
+ super();
+ }
+
+ /**
+ * Constructor which performs no validation on the passed in parameters.
+ *
+ * @param initialCapacity the initial capacity, must be a power of two
+ * @param loadFactor the load factor, must be &gt; 0.0f and generally &lt; 1.0f
+ * @param threshold the threshold, must be sensible
+ */
+ protected AbstractHashedMap(int initialCapacity, float loadFactor, int threshold) {
+ super();
+ this.loadFactor = loadFactor;
+ this.data = new HashEntry[initialCapacity];
+ this.threshold = threshold;
+ init();
+ }
+
+ /**
+ * Constructs a new, empty map with the specified initial capacity and
+ * default load factor.
+ *
+ * @param initialCapacity the initial capacity
+ * @throws IllegalArgumentException if the initial capacity is less than one
+ */
+ protected AbstractHashedMap(int initialCapacity) {
+ this(initialCapacity, DEFAULT_LOAD_FACTOR);
+ }
+
+ /**
+ * Constructs a new, empty map with the specified initial capacity and
+ * load factor.
+ *
+ * @param initialCapacity the initial capacity
+ * @param loadFactor the load factor
+ * @throws IllegalArgumentException if the initial capacity is less than one
+ * @throws IllegalArgumentException if the load factor is less than or equal to zero
+ */
+ protected AbstractHashedMap(int initialCapacity, float loadFactor) {
+ super();
+ if (initialCapacity < 1) {
+ throw new IllegalArgumentException("Initial capacity must be greater than 0");
+ }
+ if (loadFactor <= 0.0f || Float.isNaN(loadFactor)) {
+ throw new IllegalArgumentException("Load factor must be greater than 0");
+ }
+ this.loadFactor = loadFactor;
+ this.threshold = calculateThreshold(initialCapacity, loadFactor);
+ initialCapacity = calculateNewCapacity(initialCapacity);
+ this.data = new HashEntry[initialCapacity];
+ init();
+ }
+
+ /**
+ * Constructor copying elements from another map.
+ *
+ * @param map the map to copy
+ * @throws NullPointerException if the map is null
+ */
+ protected AbstractHashedMap(Map<? extends K, ? extends V> map) {
+ this(Math.max(2 * map.size(), DEFAULT_CAPACITY), DEFAULT_LOAD_FACTOR);
+ putAll(map);
+ }
+
+ /**
+ * Initialise subclasses during construction, cloning or deserialization.
+ */
+ protected void init() {
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Gets the value mapped to the key specified.
+ *
+ * @param key the key
+ * @return the mapped value, null if no match
+ */
+ public V get(Object key) {
+ int hashCode = hash((key == null) ? NULL : key);
+ HashEntry<K, V> entry = data[hashIndex(hashCode, data.length)]; // no local for hash index
+ while (entry != null) {
+ if (entry.hashCode == hashCode && isEqualKey(key, entry.key)) {
+ return entry.getValue();
+ }
+ entry = entry.next;
+ }
+ return null;
+ }
+
+ /**
+ * Gets the size of the map.
+ *
+ * @return the size
+ */
+ public int size() {
+ return size;
+ }
+
+ /**
+ * Checks whether the map is currently empty.
+ *
+ * @return true if the map is currently size zero
+ */
+ public boolean isEmpty() {
+ return (size == 0);
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Checks whether the map contains the specified key.
+ *
+ * @param key the key to search for
+ * @return true if the map contains the key
+ */
+ public boolean containsKey(Object key) {
+ int hashCode = hash((key == null) ? NULL : key);
+ HashEntry entry = data[hashIndex(hashCode, data.length)]; // no local for hash index
+ while (entry != null) {
+ if (entry.hashCode == hashCode && isEqualKey(key, entry.getKey())) {
+ return true;
+ }
+ entry = entry.next;
+ }
+ return false;
+ }
+
+ /**
+ * Checks whether the map contains the specified value.
+ *
+ * @param value the value to search for
+ * @return true if the map contains the value
+ */
+ public boolean containsValue(Object value) {
+ if (value == null) {
+ for (int i = 0, isize = data.length; i < isize; i++) {
+ HashEntry entry = data[i];
+ while (entry != null) {
+ if (entry.getValue() == null) {
+ return true;
+ }
+ entry = entry.next;
+ }
+ }
+ } else {
+ for (int i = 0, isize = data.length; i < isize; i++) {
+ HashEntry entry = data[i];
+ while (entry != null) {
+ if (isEqualValue(value, entry.getValue())) {
+ return true;
+ }
+ entry = entry.next;
+ }
+ }
+ }
+ return false;
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Puts a key-value mapping into this map.
+ *
+ * @param key the key to add
+ * @param value the value to add
+ * @return the value previously mapped to this key, null if none
+ */
+ public V put(K key, V value) {
+ int hashCode = hash((key == null) ? NULL : key);
+ int index = hashIndex(hashCode, data.length);
+ HashEntry<K, V> entry = data[index];
+ while (entry != null) {
+ if (entry.hashCode == hashCode && isEqualKey(key, entry.getKey())) {
+ V oldValue = entry.getValue();
+ updateEntry(entry, value);
+ return oldValue;
+ }
+ entry = entry.next;
+ }
+ addMapping(index, hashCode, key, value);
+ return null;
+ }
+
+ /**
+ * Puts all the values from the specified map into this map.
+ * <p/>
+ * This implementation iterates around the specified map and
+ * uses {@link #put(Object, Object)}.
+ *
+ * @param map the map to add
+ * @throws NullPointerException if the map is null
+ */
+ public void putAll(Map<? extends K, ? extends V> map) {
+ int mapSize = map.size();
+ if (mapSize == 0) {
+ return;
+ }
+ int newSize = (int) ((size + mapSize) / loadFactor + 1);
+ ensureCapacity(calculateNewCapacity(newSize));
+ // Have to cast here because of compiler inference problems.
+ for (Iterator it = map.entrySet().iterator(); it.hasNext();) {
+ Map.Entry<? extends K, ? extends V> entry = (Map.Entry<? extends K, ? extends V>) it.next();
+ put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Removes the specified mapping from this map.
+ *
+ * @param key the mapping to remove
+ * @return the value mapped to the removed key, null if key not in map
+ */
+ public V remove(Object key) {
+ int hashCode = hash((key == null) ? NULL : key);
+ int index = hashIndex(hashCode, data.length);
+ HashEntry<K, V> entry = data[index];
+ HashEntry<K, V> previous = null;
+ while (entry != null) {
+ if (entry.hashCode == hashCode && isEqualKey(key, entry.getKey())) {
+ V oldValue = entry.getValue();
+ removeMapping(entry, index, previous);
+ return oldValue;
+ }
+ previous = entry;
+ entry = entry.next;
+ }
+ return null;
+ }
+
+ /**
+ * Clears the map, resetting the size to zero and nullifying references
+ * to avoid garbage collection issues.
+ */
+ public void clear() {
+ modCount++;
+ HashEntry[] data = this.data;
+ for (int i = data.length - 1; i >= 0; i--) {
+ data[i] = null;
+ }
+ size = 0;
+ }
+
+ /**
+ * Gets the hash code for the key specified.
+ * This implementation uses the additional hashing routine from JDK1.4.
+ * Subclasses can override this to return alternate hash codes.
+ *
+ * @param key the key to get a hash code for
+ * @return the hash code
+ */
+ protected int hash(Object key) {
+ // same as JDK 1.4
+ int h = key.hashCode();
+ h += ~(h << 9);
+ h ^= (h >>> 14);
+ h += (h << 4);
+ h ^= (h >>> 10);
+ return h;
+ }
+
+ /**
+ * Compares two keys, in internal converted form, to see if they are equal.
+ * This implementation uses the equals method.
+ * Subclasses can override this to match differently.
+ *
+ * @param key1 the first key to compare passed in from outside
+ * @param key2 the second key extracted from the entry via <code>entry.key</code>
+ * @return true if equal
+ */
+ protected boolean isEqualKey(Object key1, Object key2) {
+ return (key1 == key2 || ((key1 != null) && key1.equals(key2)));
+ }
+
+ /**
+ * Compares two values, in external form, to see if they are equal.
+ * This implementation uses the equals method and assumes neither value is null.
+ * Subclasses can override this to match differently.
+ *
+ * @param value1 the first value to compare passed in from outside
+ * @param value2 the second value extracted from the entry via <code>getValue()</code>
+ * @return true if equal
+ */
+ protected boolean isEqualValue(Object value1, Object value2) {
+ return (value1 == value2 || value1.equals(value2));
+ }
+
+ /**
+ * Gets the index into the data storage for the hashCode specified.
+ * This implementation uses the least significant bits of the hashCode.
+ * Subclasses can override this to return alternate bucketing.
+ *
+ * @param hashCode the hash code to use
+ * @param dataSize the size of the data to pick a bucket from
+ * @return the bucket index
+ */
+ protected int hashIndex(int hashCode, int dataSize) {
+ return hashCode & (dataSize - 1);
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Gets the entry mapped to the key specified.
+ * <p/>
+ * This method exists for subclasses that may need to perform a multi-step
+ * process accessing the entry. The public methods in this class don't use this
+ * method to gain a small performance boost.
+ *
+ * @param key the key
+ * @return the entry, null if no match
+ */
+ protected HashEntry<K, V> getEntry(Object key) {
+ int hashCode = hash((key == null) ? NULL : key);
+ HashEntry<K, V> entry = data[hashIndex(hashCode, data.length)]; // no local for hash index
+ while (entry != null) {
+ if (entry.hashCode == hashCode && isEqualKey(key, entry.getKey())) {
+ return entry;
+ }
+ entry = entry.next;
+ }
+ return null;
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Updates an existing key-value mapping to change the value.
+ * <p/>
+ * This implementation calls <code>setValue()</code> on the entry.
+ * Subclasses could override to handle changes to the map.
+ *
+ * @param entry the entry to update
+ * @param newValue the new value to store
+ */
+ protected void updateEntry(HashEntry<K, V> entry, V newValue) {
+ entry.setValue(newValue);
+ }
+
+ /**
+ * Reuses an existing key-value mapping, storing completely new data.
+ * <p/>
+ * This implementation sets all the data fields on the entry.
+ * Subclasses could populate additional entry fields.
+ *
+ * @param entry the entry to update, not null
+ * @param hashIndex the index in the data array
+ * @param hashCode the hash code of the key to add
+ * @param key the key to add
+ * @param value the value to add
+ */
+ protected void reuseEntry(HashEntry<K, V> entry, int hashIndex, int hashCode, K key, V value) {
+ entry.next = data[hashIndex];
+ entry.hashCode = hashCode;
+ entry.key = key;
+ entry.value = value;
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Adds a new key-value mapping into this map.
+ * <p/>
+ * This implementation calls <code>createEntry()</code>, <code>addEntry()</code>
+ * and <code>checkCapacity()</code>.
+ * It also handles changes to <code>modCount</code> and <code>size</code>.
+ * Subclasses could override to fully control adds to the map.
+ *
+ * @param hashIndex the index into the data array to store at
+ * @param hashCode the hash code of the key to add
+ * @param key the key to add
+ * @param value the value to add
+ */
+ protected void addMapping(int hashIndex, int hashCode, K key, V value) {
+ modCount++;
+ HashEntry<K, V> entry = createEntry(data[hashIndex], hashCode, key, value);
+ addEntry(entry, hashIndex);
+ size++;
+ checkCapacity();
+ }
+
+ /**
+ * Creates an entry to store the key-value data.
+ * <p/>
+ * This implementation creates a new HashEntry instance.
+ * Subclasses can override this to return a different storage class,
+ * or implement caching.
+ *
+ * @param next the next entry in sequence
+ * @param hashCode the hash code to use
+ * @param key the key to store
+ * @param value the value to store
+ * @return the newly created entry
+ */
+ protected HashEntry<K, V> createEntry(HashEntry<K, V> next, int hashCode, K key, V value) {
+ return new HashEntry<K, V>(next, hashCode, key, value);
+ }
+
+ /**
+ * Adds an entry into this map.
+ * <p/>
+ * This implementation adds the entry to the data storage table.
+ * Subclasses could override to handle changes to the map.
+ *
+ * @param entry the entry to add
+ * @param hashIndex the index into the data array to store at
+ */
+ protected void addEntry(HashEntry<K, V> entry, int hashIndex) {
+ data[hashIndex] = entry;
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Removes a mapping from the map.
+ * <p/>
+ * This implementation calls <code>removeEntry()</code> and <code>destroyEntry()</code>.
+ * It also handles changes to <code>modCount</code> and <code>size</code>.
+ * Subclasses could override to fully control removals from the map.
+ *
+ * @param entry the entry to remove
+ * @param hashIndex the index into the data structure
+ * @param previous the previous entry in the chain
+ */
+ protected void removeMapping(HashEntry<K, V> entry, int hashIndex, HashEntry<K, V> previous) {
+ modCount++;
+ removeEntry(entry, hashIndex, previous);
+ size--;
+ destroyEntry(entry);
+ }
+
+ /**
+ * Removes an entry from the chain stored in a particular index.
+ * <p/>
+ * This implementation removes the entry from the data storage table.
+ * The size is not updated.
+ * Subclasses could override to handle changes to the map.
+ *
+ * @param entry the entry to remove
+ * @param hashIndex the index into the data structure
+ * @param previous the previous entry in the chain
+ */
+ protected void removeEntry(HashEntry<K, V> entry, int hashIndex, HashEntry<K, V> previous) {
+ if (previous == null) {
+ data[hashIndex] = entry.next;
+ } else {
+ previous.next = entry.next;
+ }
+ }
+
+ /**
+ * Kills an entry ready for the garbage collector.
+ * <p/>
+ * This implementation prepares the HashEntry for garbage collection.
+ * Subclasses can override this to implement caching (override clear as well).
+ *
+ * @param entry the entry to destroy
+ */
+ protected void destroyEntry(HashEntry<K, V> entry) {
+ entry.next = null;
+ entry.key = null;
+ entry.value = null;
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Checks the capacity of the map and enlarges it if necessary.
+ * <p/>
+ * This implementation uses the threshold to check if the map needs enlarging
+ */
+ protected void checkCapacity() {
+ if (size >= threshold) {
+ int newCapacity = data.length * 2;
+ if (newCapacity <= MAXIMUM_CAPACITY) {
+ ensureCapacity(newCapacity);
+ }
+ }
+ }
+
+ /**
+ * Changes the size of the data structure to the capacity proposed.
+ *
+ * @param newCapacity the new capacity of the array (a power of two, less or equal to max)
+ */
+ protected void ensureCapacity(int newCapacity) {
+ int oldCapacity = data.length;
+ if (newCapacity <= oldCapacity) {
+ return;
+ }
+ if (size == 0) {
+ threshold = calculateThreshold(newCapacity, loadFactor);
+ data = new HashEntry[newCapacity];
+ } else {
+ HashEntry<K, V> oldEntries[] = data;
+ HashEntry<K, V> newEntries[] = new HashEntry[newCapacity];
+
+ modCount++;
+ for (int i = oldCapacity - 1; i >= 0; i--) {
+ HashEntry<K, V> entry = oldEntries[i];
+ if (entry != null) {
+ oldEntries[i] = null; // gc
+ do {
+ HashEntry<K, V> next = entry.next;
+ int index = hashIndex(entry.hashCode, newCapacity);
+ entry.next = newEntries[index];
+ newEntries[index] = entry;
+ entry = next;
+ } while (entry != null);
+ }
+ }
+ threshold = calculateThreshold(newCapacity, loadFactor);
+ data = newEntries;
+ }
+ }
+
+ /**
+ * Calculates the new capacity of the map.
+ * This implementation normalizes the capacity to a power of two.
+ *
+ * @param proposedCapacity the proposed capacity
+ * @return the normalized new capacity
+ */
+ protected int calculateNewCapacity(int proposedCapacity) {
+ int newCapacity = 1;
+ if (proposedCapacity > MAXIMUM_CAPACITY) {
+ newCapacity = MAXIMUM_CAPACITY;
+ } else {
+ while (newCapacity < proposedCapacity) {
+ newCapacity <<= 1; // multiply by two
+ }
+ if (newCapacity > MAXIMUM_CAPACITY) {
+ newCapacity = MAXIMUM_CAPACITY;
+ }
+ }
+ return newCapacity;
+ }
+
+ /**
+ * Calculates the new threshold of the map, where it will be resized.
+ * This implementation uses the load factor.
+ *
+ * @param newCapacity the new capacity
+ * @param factor the load factor
+ * @return the new resize threshold
+ */
+ protected int calculateThreshold(int newCapacity, float factor) {
+ return (int) (newCapacity * factor);
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Gets the <code>next</code> field from a <code>HashEntry</code>.
+ * Used in subclasses that have no visibility of the field.
+ *
+ * @param entry the entry to query, must not be null
+ * @return the <code>next</code> field of the entry
+ * @throws NullPointerException if the entry is null
+ * @since Commons Collections 3.1
+ */
+ protected HashEntry<K, V> entryNext(HashEntry<K, V> entry) {
+ return entry.next;
+ }
+
+ /**
+ * Gets the <code>hashCode</code> field from a <code>HashEntry</code>.
+ * Used in subclasses that have no visibility of the field.
+ *
+ * @param entry the entry to query, must not be null
+ * @return the <code>hashCode</code> field of the entry
+ * @throws NullPointerException if the entry is null
+ * @since Commons Collections 3.1
+ */
+ protected int entryHashCode(HashEntry<K, V> entry) {
+ return entry.hashCode;
+ }
+
+ /**
+ * Gets the <code>key</code> field from a <code>HashEntry</code>.
+ * Used in subclasses that have no visibility of the field.
+ *
+ * @param entry the entry to query, must not be null
+ * @return the <code>key</code> field of the entry
+ * @throws NullPointerException if the entry is null
+ * @since Commons Collections 3.1
+ */
+ protected K entryKey(HashEntry<K, V> entry) {
+ return entry.key;
+ }
+
+ /**
+ * Gets the <code>value</code> field from a <code>HashEntry</code>.
+ * Used in subclasses that have no visibility of the field.
+ *
+ * @param entry the entry to query, must not be null
+ * @return the <code>value</code> field of the entry
+ * @throws NullPointerException if the entry is null
+ * @since Commons Collections 3.1
+ */
+ protected V entryValue(HashEntry<K, V> entry) {
+ return entry.value;
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Gets an iterator over the map.
+ * Changes made to the iterator affect this map.
+ * <p/>
+ * A MapIterator returns the keys in the map. It also provides convenient
+ * methods to get the key and value, and set the value.
+ * It avoids the need to create an entrySet/keySet/values object.
+ * It also avoids creating the Map.Entry object.
+ *
+ * @return the map iterator
+ */
+ public MapIterator<K, V> mapIterator() {
+ if (size == 0) {
+ return EmptyMapIterator.INSTANCE;
+ }
+ return new HashMapIterator<K, V>(this);
+ }
+
+ /**
+ * MapIterator implementation.
+ */
+ protected static class HashMapIterator <K,V> extends HashIterator<K, V> implements MapIterator<K, V> {
+
+ protected HashMapIterator(AbstractHashedMap<K, V> parent) {
+ super(parent);
+ }
+
+ public K next() {
+ return super.nextEntry().getKey();
+ }
+
+ public K getKey() {
+ HashEntry<K, V> current = currentEntry();
+ if (current == null) {
+ throw new IllegalStateException(AbstractHashedMap.GETKEY_INVALID);
+ }
+ return current.getKey();
+ }
+
+ public V getValue() {
+ HashEntry<K, V> current = currentEntry();
+ if (current == null) {
+ throw new IllegalStateException(AbstractHashedMap.GETVALUE_INVALID);
+ }
+ return current.getValue();
+ }
+
+ public V setValue(V value) {
+ HashEntry<K, V> current = currentEntry();
+ if (current == null) {
+ throw new IllegalStateException(AbstractHashedMap.SETVALUE_INVALID);
+ }
+ return current.setValue(value);
+ }
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Gets the entrySet view of the map.
+ * Changes made to the view affect this map.
+ * To simply iterate through the entries, use {@link #mapIterator()}.
+ *
+ * @return the entrySet view
+ */
+ public Set<Map.Entry<K, V>> entrySet() {
+ if (entrySet == null) {
+ entrySet = new EntrySet<K, V>(this);
+ }
+ return entrySet;
+ }
+
+ /**
+ * Creates an entry set iterator.
+ * Subclasses can override this to return iterators with different properties.
+ *
+ * @return the entrySet iterator
+ */
+ protected Iterator<Map.Entry<K, V>> createEntrySetIterator() {
+ if (size() == 0) {
+ return EmptyIterator.INSTANCE;
+ }
+ return new EntrySetIterator<K, V>(this);
+ }
+
+ /**
+ * EntrySet implementation.
+ */
+ protected static class EntrySet <K,V> extends AbstractSet<Map.Entry<K, V>> {
+ /**
+ * The parent map
+ */
+ protected final AbstractHashedMap<K, V> parent;
+
+ protected EntrySet(AbstractHashedMap<K, V> parent) {
+ super();
+ this.parent = parent;
+ }
+
+ public int size() {
+ return parent.size();
+ }
+
+ public void clear() {
+ parent.clear();
+ }
+
+ public boolean contains(Map.Entry<K, V> entry) {
+ Map.Entry<K, V> e = entry;
+ Entry<K, V> match = parent.getEntry(e.getKey());
+ return (match != null && match.equals(e));
+ }
+
+ public boolean remove(Object obj) {
+ if (obj instanceof Map.Entry == false) {
+ return false;
+ }
+ if (contains(obj) == false) {
+ return false;
+ }
+ Map.Entry<K, V> entry = (Map.Entry<K, V>) obj;
+ K key = entry.getKey();
+ parent.remove(key);
+ return true;
+ }
+
+ public Iterator<Map.Entry<K, V>> iterator() {
+ return parent.createEntrySetIterator();
+ }
+ }
+
+ /**
+ * EntrySet iterator.
+ */
+ protected static class EntrySetIterator <K,V> extends HashIterator<K, V> implements Iterator<Map.Entry<K, V>> {
+
+ protected EntrySetIterator(AbstractHashedMap<K, V> parent) {
+ super(parent);
+ }
+
+ public HashEntry<K, V> next() {
+ return super.nextEntry();
+ }
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Gets the keySet view of the map.
+ * Changes made to the view affect this map.
+ * To simply iterate through the keys, use {@link #mapIterator()}.
+ *
+ * @return the keySet view
+ */
+ public Set<K> keySet() {
+ if (keySet == null) {
+ keySet = new KeySet<K, V>(this);
+ }
+ return keySet;
+ }
+
+ /**
+ * Creates a key set iterator.
+ * Subclasses can override this to return iterators with different properties.
+ *
+ * @return the keySet iterator
+ */
+ protected Iterator<K> createKeySetIterator() {
+ if (size() == 0) {
+ return EmptyIterator.INSTANCE;
+ }
+ return new KeySetIterator<K, V>(this);
+ }
+
+ /**
+ * KeySet implementation.
+ */
+ protected static class KeySet <K,V> extends AbstractSet<K> {
+ /**
+ * The parent map
+ */
+ protected final AbstractHashedMap<K, V> parent;
+
+ protected KeySet(AbstractHashedMap<K, V> parent) {
+ super();
+ this.parent = parent;
+ }
+
+ public int size() {
+ return parent.size();
+ }
+
+ public void clear() {
+ parent.clear();
+ }
+
+ public boolean contains(Object key) {
+ return parent.containsKey(key);
+ }
+
+ public boolean remove(Object key) {
+ boolean result = parent.containsKey(key);
+ parent.remove(key);
+ return result;
+ }
+
+ public Iterator<K> iterator() {
+ return parent.createKeySetIterator();
+ }
+ }
+
+ /**
+ * KeySet iterator.
+ */
+ protected static class KeySetIterator <K,V> extends HashIterator<K, V> implements Iterator<K> {
+
+ protected KeySetIterator(AbstractHashedMap<K, V> parent) {
+ super(parent);
+ }
+
+ public K next() {
+ return super.nextEntry().getKey();
+ }
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Gets the values view of the map.
+ * Changes made to the view affect this map.
+ * To simply iterate through the values, use {@link #mapIterator()}.
+ *
+ * @return the values view
+ */
+ public Collection<V> values() {
+ if (values == null) {
+ values = new Values(this);
+ }
+ return values;
+ }
+
+ /**
+ * Creates a values iterator.
+ * Subclasses can override this to return iterators with different properties.
+ *
+ * @return the values iterator
+ */
+ protected Iterator<V> createValuesIterator() {
+ if (size() == 0) {
+ return EmptyIterator.INSTANCE;
+ }
+ return new ValuesIterator<K, V>(this);
+ }
+
+ /**
+ * Values implementation.
+ */
+ protected static class Values <K,V> extends AbstractCollection<V> {
+ /**
+ * The parent map
+ */
+ protected final AbstractHashedMap<K, V> parent;
+
+ protected Values(AbstractHashedMap<K, V> parent) {
+ super();
+ this.parent = parent;
+ }
+
+ public int size() {
+ return parent.size();
+ }
+
+ public void clear() {
+ parent.clear();
+ }
+
+ public boolean contains(Object value) {
+ return parent.containsValue(value);
+ }
+
+ public Iterator<V> iterator() {
+ return parent.createValuesIterator();
+ }
+ }
+
+ /**
+ * Values iterator.
+ */
+ protected static class ValuesIterator <K,V> extends HashIterator<K, V> implements Iterator<V> {
+
+ protected ValuesIterator(AbstractHashedMap<K, V> parent) {
+ super(parent);
+ }
+
+ public V next() {
+ return super.nextEntry().getValue();
+ }
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * HashEntry used to store the data.
+ * <p/>
+ * If you subclass <code>AbstractHashedMap</code> but not <code>HashEntry</code>
+ * then you will not be able to access the protected fields.
+ * The <code>entryXxx()</code> methods on <code>AbstractHashedMap</code> exist
+ * to provide the necessary access.
+ */
+ protected static class HashEntry <K,V> implements Map.Entry<K, V>, KeyValue<K, V> {
+ /**
+ * The next entry in the hash chain
+ */
+ protected HashEntry<K, V> next;
+ /**
+ * The hash code of the key
+ */
+ protected int hashCode;
+ /**
+ * The key
+ */
+ private K key;
+ /**
+ * The value
+ */
+ private V value;
+
+ protected HashEntry(HashEntry<K, V> next, int hashCode, K key, V value) {
+ super();
+ this.next = next;
+ this.hashCode = hashCode;
+ this.key = key;
+ this.value = value;
+ }
+
+ public K getKey() {
+ return key;
+ }
+
+ public void setKey(K key) {
+ this.key = key;
+ }
+
+ public V getValue() {
+ return value;
+ }
+
+ public V setValue(V value) {
+ V old = this.value;
+ this.value = value;
+ return old;
+ }
+
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj instanceof Map.Entry == false) {
+ return false;
+ }
+ Map.Entry other = (Map.Entry) obj;
+ return (getKey() == null ? other.getKey() == null : getKey().equals(other.getKey())) && (getValue() == null ? other.getValue() == null : getValue().equals(other.getValue()));
+ }
+
+ public int hashCode() {
+ return (getKey() == null ? 0 : getKey().hashCode()) ^ (getValue() == null ? 0 : getValue().hashCode());
+ }
+
+ public String toString() {
+ return new StringBuilder().append(getKey()).append('=').append(getValue()).toString();
+ }
+ }
+
+ /**
+ * Base Iterator
+ */
+ protected static abstract class HashIterator <K,V> {
+
+ /**
+ * The parent map
+ */
+ protected final AbstractHashedMap parent;
+ /**
+ * The current index into the array of buckets
+ */
+ protected int hashIndex;
+ /**
+ * The last returned entry
+ */
+ protected HashEntry<K, V> last;
+ /**
+ * The next entry
+ */
+ protected HashEntry<K, V> next;
+ /**
+ * The modification count expected
+ */
+ protected int expectedModCount;
+
+ protected HashIterator(AbstractHashedMap<K, V> parent) {
+ super();
+ this.parent = parent;
+ HashEntry<K, V>[] data = parent.data;
+ int i = data.length;
+ HashEntry<K, V> next = null;
+ while (i > 0 && next == null) {
+ next = data[--i];
+ }
+ this.next = next;
+ this.hashIndex = i;
+ this.expectedModCount = parent.modCount;
+ }
+
+ public boolean hasNext() {
+ return (next != null);
+ }
+
+ protected HashEntry<K, V> nextEntry() {
+ if (parent.modCount != expectedModCount) {
+ throw new ConcurrentModificationException();
+ }
+ HashEntry<K, V> newCurrent = next;
+ if (newCurrent == null) {
+ throw new NoSuchElementException(AbstractHashedMap.NO_NEXT_ENTRY);
+ }
+ HashEntry<K, V>[] data = parent.data;
+ int i = hashIndex;
+ HashEntry<K, V> n = newCurrent.next;
+ while (n == null && i > 0) {
+ n = data[--i];
+ }
+ next = n;
+ hashIndex = i;
+ last = newCurrent;
+ return newCurrent;
+ }
+
+ protected HashEntry<K, V> currentEntry() {
+ return last;
+ }
+
+ public void remove() {
+ if (last == null) {
+ throw new IllegalStateException(AbstractHashedMap.REMOVE_INVALID);
+ }
+ if (parent.modCount != expectedModCount) {
+ throw new ConcurrentModificationException();
+ }
+ parent.remove(last.getKey());
+ last = null;
+ expectedModCount = parent.modCount;
+ }
+
+ public String toString() {
+ if (last != null) {
+ return "Iterator[" + last.getKey() + "=" + last.getValue() + "]";
+ } else {
+ return "Iterator[]";
+ }
+ }
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Writes the map data to the stream. This method must be overridden if a
+ * subclass must be setup before <code>put()</code> is used.
+ * <p/>
+ * Serialization is not one of the JDK's nicest topics. Normal serialization will
+ * initialise the superclass before the subclass. Sometimes however, this isn't
+ * what you want, as in this case the <code>put()</code> method on read can be
+ * affected by subclass state.
+ * <p/>
+ * The solution adopted here is to serialize the state data of this class in
+ * this protected method. This method must be called by the
+ * <code>writeObject()</code> of the first serializable subclass.
+ * <p/>
+ * Subclasses may override if they have a specific field that must be present
+ * on read before this implementation will work. Generally, the read determines
+ * what must be serialized here, if anything.
+ *
+ * @param out the output stream
+ */
+ protected void doWriteObject(ObjectOutputStream out) throws IOException {
+ out.writeFloat(loadFactor);
+ out.writeInt(data.length);
+ out.writeInt(size);
+ for (MapIterator it = mapIterator(); it.hasNext();) {
+ out.writeObject(it.next());
+ out.writeObject(it.getValue());
+ }
+ }
+
+ /**
+ * Reads the map data from the stream. This method must be overridden if a
+ * subclass must be setup before <code>put()</code> is used.
+ * <p/>
+ * Serialization is not one of the JDK's nicest topics. Normal serialization will
+ * initialise the superclass before the subclass. Sometimes however, this isn't
+ * what you want, as in this case the <code>put()</code> method on read can be
+ * affected by subclass state.
+ * <p/>
+ * The solution adopted here is to deserialize the state data of this class in
+ * this protected method. This method must be called by the
+ * <code>readObject()</code> of the first serializable subclass.
+ * <p/>
+ * Subclasses may override if the subclass has a specific field that must be present
+ * before <code>put()</code> or <code>calculateThreshold()</code> will work correctly.
+ *
+ * @param in the input stream
+ */
+ protected void doReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ loadFactor = in.readFloat();
+ int capacity = in.readInt();
+ int size = in.readInt();
+ init();
+ data = new HashEntry[capacity];
+ for (int i = 0; i < size; i++) {
+ K key = (K) in.readObject();
+ V value = (V) in.readObject();
+ put(key, value);
+ }
+ threshold = calculateThreshold(data.length, loadFactor);
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Clones the map without cloning the keys or values.
+ * <p/>
+ * To implement <code>clone()</code>, a subclass must implement the
+ * <code>Cloneable</code> interface and make this method public.
+ *
+ * @return a shallow clone
+ */
+ protected Object clone() {
+ try {
+ AbstractHashedMap cloned = (AbstractHashedMap) super.clone();
+ cloned.data = new HashEntry[data.length];
+ cloned.entrySet = null;
+ cloned.keySet = null;
+ cloned.values = null;
+ cloned.modCount = 0;
+ cloned.size = 0;
+ cloned.init();
+ cloned.putAll(this);
+ return cloned;
+
+ } catch (CloneNotSupportedException ex) {
+ return null; // should never happen
+ }
+ }
+
+ /**
+ * Compares this map with another.
+ *
+ * @param obj the object to compare to
+ * @return true if equal
+ */
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj instanceof Map == false) {
+ return false;
+ }
+ Map map = (Map) obj;
+ if (map.size() != size()) {
+ return false;
+ }
+ MapIterator it = mapIterator();
+ try {
+ while (it.hasNext()) {
+ Object key = it.next();
+ Object value = it.getValue();
+ if (value == null) {
+ if (map.get(key) != null || map.containsKey(key) == false) {
+ return false;
+ }
+ } else {
+ if (value.equals(map.get(key)) == false) {
+ return false;
+ }
+ }
+ }
+ } catch (ClassCastException ignored) {
+ return false;
+ } catch (NullPointerException ignored) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Gets the standard Map hashCode.
+ *
+ * @return the hash code defined in the Map interface
+ */
+ public int hashCode() {
+ int total = 0;
+ Iterator it = createEntrySetIterator();
+ while (it.hasNext()) {
+ total += it.next().hashCode();
+ }
+ return total;
+ }
+
+ /**
+ * Gets the map as a String.
+ *
+ * @return a string version of the map
+ */
+ public String toString() {
+ if (size() == 0) {
+ return "{}";
+ }
+ StringBuilder buf = new StringBuilder(32 * size());
+ buf.append('{');
+
+ MapIterator it = mapIterator();
+ boolean hasNext = it.hasNext();
+ while (hasNext) {
+ Object key = it.next();
+ Object value = it.getValue();
+ buf.append(key == this ? "(this Map)" : key).append('=').append(value == this ? "(this Map)" : value);
+
+ hasNext = it.hasNext();
+ if (hasNext) {
+ buf.append(',').append(' ');
+ }
+ }
+
+ buf.append('}');
+ return buf.toString();
+ }
+}
diff --git a/src/org/jivesoftware/smack/util/collections/AbstractKeyValue.java b/src/org/jivesoftware/smack/util/collections/AbstractKeyValue.java
new file mode 100644
index 0000000..decc342
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/collections/AbstractKeyValue.java
@@ -0,0 +1,80 @@
+// GenericsNote: Converted.
+/*
+ * Copyright 2003-2004 The Apache Software Foundation
+ *
+ * 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.collections;
+
+
+/**
+ * Abstract pair class to assist with creating KeyValue and MapEntry implementations.
+ *
+ * @author James Strachan
+ * @author Michael A. Smith
+ * @author Neil O'Toole
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $
+ * @since Commons Collections 3.0
+ */
+public abstract class AbstractKeyValue <K,V> implements KeyValue<K, V> {
+
+ /**
+ * The key
+ */
+ protected K key;
+ /**
+ * The value
+ */
+ protected V value;
+
+ /**
+ * Constructs a new pair with the specified key and given value.
+ *
+ * @param key the key for the entry, may be null
+ * @param value the value for the entry, may be null
+ */
+ protected AbstractKeyValue(K key, V value) {
+ super();
+ this.key = key;
+ this.value = value;
+ }
+
+ /**
+ * Gets the key from the pair.
+ *
+ * @return the key
+ */
+ public K getKey() {
+ return key;
+ }
+
+ /**
+ * Gets the value from the pair.
+ *
+ * @return the value
+ */
+ public V getValue() {
+ return value;
+ }
+
+ /**
+ * Gets a debugging String view of the pair.
+ *
+ * @return a String view of the entry
+ */
+ public String toString() {
+ return new StringBuilder().append(getKey()).append('=').append(getValue()).toString();
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/util/collections/AbstractMapEntry.java b/src/org/jivesoftware/smack/util/collections/AbstractMapEntry.java
new file mode 100644
index 0000000..2feb308
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/collections/AbstractMapEntry.java
@@ -0,0 +1,89 @@
+// GenericsNote: Converted.
+/*
+ * Copyright 2003-2004 The Apache Software Foundation
+ *
+ * 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.collections;
+
+import java.util.Map;
+
+/**
+ * Abstract Pair class to assist with creating correct Map Entry implementations.
+ *
+ * @author James Strachan
+ * @author Michael A. Smith
+ * @author Neil O'Toole
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $
+ * @since Commons Collections 3.0
+ */
+public abstract class AbstractMapEntry <K,V> extends AbstractKeyValue<K, V> implements Map.Entry<K, V> {
+
+ /**
+ * Constructs a new entry with the given key and given value.
+ *
+ * @param key the key for the entry, may be null
+ * @param value the value for the entry, may be null
+ */
+ protected AbstractMapEntry(K key, V value) {
+ super(key, value);
+ }
+
+ // Map.Entry interface
+ //-------------------------------------------------------------------------
+ /**
+ * Sets the value stored in this Map Entry.
+ * <p/>
+ * This Map Entry is not connected to a Map, so only the local data is changed.
+ *
+ * @param value the new value
+ * @return the previous value
+ */
+ public V setValue(V value) {
+ V answer = this.value;
+ this.value = value;
+ return answer;
+ }
+
+ /**
+ * Compares this Map Entry with another Map Entry.
+ * <p/>
+ * Implemented per API documentation of {@link java.util.Map.Entry#equals(Object)}
+ *
+ * @param obj the object to compare to
+ * @return true if equal key and value
+ */
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj instanceof Map.Entry == false) {
+ return false;
+ }
+ Map.Entry other = (Map.Entry) obj;
+ return (getKey() == null ? other.getKey() == null : getKey().equals(other.getKey())) && (getValue() == null ? other.getValue() == null : getValue().equals(other.getValue()));
+ }
+
+ /**
+ * Gets a hashCode compatible with the equals method.
+ * <p/>
+ * Implemented per API documentation of {@link java.util.Map.Entry#hashCode()}
+ *
+ * @return a suitable hash code
+ */
+ public int hashCode() {
+ return (getKey() == null ? 0 : getKey().hashCode()) ^ (getValue() == null ? 0 : getValue().hashCode());
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/util/collections/AbstractReferenceMap.java b/src/org/jivesoftware/smack/util/collections/AbstractReferenceMap.java
new file mode 100644
index 0000000..b57f17d
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/collections/AbstractReferenceMap.java
@@ -0,0 +1,1025 @@
+// Converted, with some major refactors required. Not as memory-efficient as before, could use additional refactoring.
+// Perhaps use four different types of HashEntry classes for max efficiency:
+// normal HashEntry for HARD,HARD
+// HardRefEntry for HARD,(SOFT|WEAK)
+// RefHardEntry for (SOFT|WEAK),HARD
+// RefRefEntry for (SOFT|WEAK),(SOFT|WEAK)
+/*
+ * Copyright 2002-2004 The Apache Software Foundation
+ *
+ * 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.collections;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.lang.ref.WeakReference;
+import java.util.*;
+
+/**
+ * An abstract implementation of a hash-based map that allows the entries to
+ * be removed by the garbage collector.
+ * <p/>
+ * This class implements all the features necessary for a subclass reference
+ * hash-based map. Key-value entries are stored in instances of the
+ * <code>ReferenceEntry</code> class which can be overridden and replaced.
+ * The iterators can similarly be replaced, without the need to replace the KeySet,
+ * EntrySet and Values view classes.
+ * <p/>
+ * Overridable methods are provided to change the default hashing behaviour, and
+ * to change how entries are added to and removed from the map. Hopefully, all you
+ * need for unusual subclasses is here.
+ * <p/>
+ * When you construct an <code>AbstractReferenceMap</code>, you can specify what
+ * kind of references are used to store the map's keys and values.
+ * If non-hard references are used, then the garbage collector can remove
+ * mappings if a key or value becomes unreachable, or if the JVM's memory is
+ * running low. For information on how the different reference types behave,
+ * see {@link Reference}.
+ * <p/>
+ * Different types of references can be specified for keys and values.
+ * The keys can be configured to be weak but the values hard,
+ * in which case this class will behave like a
+ * <a href="http://java.sun.com/j2se/1.4/docs/api/java/util/WeakHashMap.html">
+ * <code>WeakHashMap</code></a>. However, you can also specify hard keys and
+ * weak values, or any other combination. The default constructor uses
+ * hard keys and soft values, providing a memory-sensitive cache.
+ * <p/>
+ * This {@link Map} implementation does <i>not</i> allow null elements.
+ * Attempting to add a null key or value to the map will raise a
+ * <code>NullPointerException</code>.
+ * <p/>
+ * All the available iterators can be reset back to the start by casting to
+ * <code>ResettableIterator</code> and calling <code>reset()</code>.
+ * <p/>
+ * This implementation is not synchronized.
+ * You can use {@link java.util.Collections#synchronizedMap} to
+ * provide synchronized access to a <code>ReferenceMap</code>.
+ *
+ * @author Paul Jack
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $
+ * @see java.lang.ref.Reference
+ * @since Commons Collections 3.1 (extracted from ReferenceMap in 3.0)
+ */
+public abstract class AbstractReferenceMap <K,V> extends AbstractHashedMap<K, V> {
+
+ /**
+ * Constant indicating that hard references should be used
+ */
+ public static final int HARD = 0;
+
+ /**
+ * Constant indicating that soft references should be used
+ */
+ public static final int SOFT = 1;
+
+ /**
+ * Constant indicating that weak references should be used
+ */
+ public static final int WEAK = 2;
+
+ /**
+ * The reference type for keys. Must be HARD, SOFT, WEAK.
+ *
+ * @serial
+ */
+ protected int keyType;
+
+ /**
+ * The reference type for values. Must be HARD, SOFT, WEAK.
+ *
+ * @serial
+ */
+ protected int valueType;
+
+ /**
+ * Should the value be automatically purged when the associated key has been collected?
+ */
+ protected boolean purgeValues;
+
+ /**
+ * ReferenceQueue used to eliminate stale mappings.
+ * See purge.
+ */
+ private transient ReferenceQueue queue;
+
+ //-----------------------------------------------------------------------
+ /**
+ * Constructor used during deserialization.
+ */
+ protected AbstractReferenceMap() {
+ super();
+ }
+
+ /**
+ * Constructs a new empty map with the specified reference types,
+ * load factor and initial capacity.
+ *
+ * @param keyType the type of reference to use for keys;
+ * must be {@link #SOFT} or {@link #WEAK}
+ * @param valueType the type of reference to use for values;
+ * must be {@link #SOFT} or {@link #WEAK}
+ * @param capacity the initial capacity for the map
+ * @param loadFactor the load factor for the map
+ * @param purgeValues should the value be automatically purged when the
+ * key is garbage collected
+ */
+ protected AbstractReferenceMap(int keyType, int valueType, int capacity, float loadFactor, boolean purgeValues) {
+ super(capacity, loadFactor);
+ verify("keyType", keyType);
+ verify("valueType", valueType);
+ this.keyType = keyType;
+ this.valueType = valueType;
+ this.purgeValues = purgeValues;
+ }
+
+ /**
+ * Initialise this subclass during construction, cloning or deserialization.
+ */
+ protected void init() {
+ queue = new ReferenceQueue();
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Checks the type int is a valid value.
+ *
+ * @param name the name for error messages
+ * @param type the type value to check
+ * @throws IllegalArgumentException if the value if invalid
+ */
+ private static void verify(String name, int type) {
+ if ((type < HARD) || (type > WEAK)) {
+ throw new IllegalArgumentException(name + " must be HARD, SOFT, WEAK.");
+ }
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Gets the size of the map.
+ *
+ * @return the size
+ */
+ public int size() {
+ purgeBeforeRead();
+ return super.size();
+ }
+
+ /**
+ * Checks whether the map is currently empty.
+ *
+ * @return true if the map is currently size zero
+ */
+ public boolean isEmpty() {
+ purgeBeforeRead();
+ return super.isEmpty();
+ }
+
+ /**
+ * Checks whether the map contains the specified key.
+ *
+ * @param key the key to search for
+ * @return true if the map contains the key
+ */
+ public boolean containsKey(Object key) {
+ purgeBeforeRead();
+ Entry entry = getEntry(key);
+ if (entry == null) {
+ return false;
+ }
+ return (entry.getValue() != null);
+ }
+
+ /**
+ * Checks whether the map contains the specified value.
+ *
+ * @param value the value to search for
+ * @return true if the map contains the value
+ */
+ public boolean containsValue(Object value) {
+ purgeBeforeRead();
+ if (value == null) {
+ return false;
+ }
+ return super.containsValue(value);
+ }
+
+ /**
+ * Gets the value mapped to the key specified.
+ *
+ * @param key the key
+ * @return the mapped value, null if no match
+ */
+ public V get(Object key) {
+ purgeBeforeRead();
+ Entry<K, V> entry = getEntry(key);
+ if (entry == null) {
+ return null;
+ }
+ return entry.getValue();
+ }
+
+
+ /**
+ * Puts a key-value mapping into this map.
+ * Neither the key nor the value may be null.
+ *
+ * @param key the key to add, must not be null
+ * @param value the value to add, must not be null
+ * @return the value previously mapped to this key, null if none
+ * @throws NullPointerException if either the key or value is null
+ */
+ public V put(K key, V value) {
+ if (key == null) {
+ throw new NullPointerException("null keys not allowed");
+ }
+ if (value == null) {
+ throw new NullPointerException("null values not allowed");
+ }
+
+ purgeBeforeWrite();
+ return super.put(key, value);
+ }
+
+ /**
+ * Removes the specified mapping from this map.
+ *
+ * @param key the mapping to remove
+ * @return the value mapped to the removed key, null if key not in map
+ */
+ public V remove(Object key) {
+ if (key == null) {
+ return null;
+ }
+ purgeBeforeWrite();
+ return super.remove(key);
+ }
+
+ /**
+ * Clears this map.
+ */
+ public void clear() {
+ super.clear();
+ while (queue.poll() != null) {
+ } // drain the queue
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Gets a MapIterator over the reference map.
+ * The iterator only returns valid key/value pairs.
+ *
+ * @return a map iterator
+ */
+ public MapIterator<K, V> mapIterator() {
+ return new ReferenceMapIterator<K, V>(this);
+ }
+
+ /**
+ * Returns a set view of this map's entries.
+ * An iterator returned entry is valid until <code>next()</code> is called again.
+ * The <code>setValue()</code> method on the <code>toArray</code> entries has no effect.
+ *
+ * @return a set view of this map's entries
+ */
+ public Set<Map.Entry<K, V>> entrySet() {
+ if (entrySet == null) {
+ entrySet = new ReferenceEntrySet<K, V>(this);
+ }
+ return entrySet;
+ }
+
+ /**
+ * Returns a set view of this map's keys.
+ *
+ * @return a set view of this map's keys
+ */
+ public Set<K> keySet() {
+ if (keySet == null) {
+ keySet = new ReferenceKeySet<K, V>(this);
+ }
+ return keySet;
+ }
+
+ /**
+ * Returns a collection view of this map's values.
+ *
+ * @return a set view of this map's values
+ */
+ public Collection<V> values() {
+ if (values == null) {
+ values = new ReferenceValues<K, V>(this);
+ }
+ return values;
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Purges stale mappings from this map before read operations.
+ * <p/>
+ * This implementation calls {@link #purge()} to maintain a consistent state.
+ */
+ protected void purgeBeforeRead() {
+ purge();
+ }
+
+ /**
+ * Purges stale mappings from this map before write operations.
+ * <p/>
+ * This implementation calls {@link #purge()} to maintain a consistent state.
+ */
+ protected void purgeBeforeWrite() {
+ purge();
+ }
+
+ /**
+ * Purges stale mappings from this map.
+ * <p/>
+ * Note that this method is not synchronized! Special
+ * care must be taken if, for instance, you want stale
+ * mappings to be removed on a periodic basis by some
+ * background thread.
+ */
+ protected void purge() {
+ Reference ref = queue.poll();
+ while (ref != null) {
+ purge(ref);
+ ref = queue.poll();
+ }
+ }
+
+ /**
+ * Purges the specified reference.
+ *
+ * @param ref the reference to purge
+ */
+ protected void purge(Reference ref) {
+ // The hashCode of the reference is the hashCode of the
+ // mapping key, even if the reference refers to the
+ // mapping value...
+ int hash = ref.hashCode();
+ int index = hashIndex(hash, data.length);
+ HashEntry<K, V> previous = null;
+ HashEntry<K, V> entry = data[index];
+ while (entry != null) {
+ if (((ReferenceEntry<K, V>) entry).purge(ref)) {
+ if (previous == null) {
+ data[index] = entry.next;
+ } else {
+ previous.next = entry.next;
+ }
+ this.size--;
+ return;
+ }
+ previous = entry;
+ entry = entry.next;
+ }
+
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Gets the entry mapped to the key specified.
+ *
+ * @param key the key
+ * @return the entry, null if no match
+ */
+ protected HashEntry<K, V> getEntry(Object key) {
+ if (key == null) {
+ return null;
+ } else {
+ return super.getEntry(key);
+ }
+ }
+
+ /**
+ * Gets the hash code for a MapEntry.
+ * Subclasses can override this, for example to use the identityHashCode.
+ *
+ * @param key the key to get a hash code for, may be null
+ * @param value the value to get a hash code for, may be null
+ * @return the hash code, as per the MapEntry specification
+ */
+ protected int hashEntry(Object key, Object value) {
+ return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode());
+ }
+
+ /**
+ * Compares two keys, in internal converted form, to see if they are equal.
+ * <p/>
+ * This implementation converts the key from the entry to a real reference
+ * before comparison.
+ *
+ * @param key1 the first key to compare passed in from outside
+ * @param key2 the second key extracted from the entry via <code>entry.key</code>
+ * @return true if equal
+ */
+ protected boolean isEqualKey(Object key1, Object key2) {
+ //if ((key1 == null) && (key2 != null) || (key1 != null) || (key2 == null)) {
+ // return false;
+ //}
+ // GenericsNote: Conversion from reference handled by getKey() which replaced all .key references
+ //key2 = (keyType > HARD ? ((Reference) key2).get() : key2);
+ return (key1 == key2 || key1.equals(key2));
+ }
+
+ /**
+ * Creates a ReferenceEntry instead of a HashEntry.
+ *
+ * @param next the next entry in sequence
+ * @param hashCode the hash code to use
+ * @param key the key to store
+ * @param value the value to store
+ * @return the newly created entry
+ */
+ public HashEntry<K, V> createEntry(HashEntry<K, V> next, int hashCode, K key, V value) {
+ return new ReferenceEntry<K, V>(this, (ReferenceEntry<K, V>) next, hashCode, key, value);
+ }
+
+ /**
+ * Creates an entry set iterator.
+ *
+ * @return the entrySet iterator
+ */
+ protected Iterator<Map.Entry<K, V>> createEntrySetIterator() {
+ return new ReferenceEntrySetIterator<K, V>(this);
+ }
+
+ /**
+ * Creates an key set iterator.
+ *
+ * @return the keySet iterator
+ */
+ protected Iterator<K> createKeySetIterator() {
+ return new ReferenceKeySetIterator<K, V>(this);
+ }
+
+ /**
+ * Creates an values iterator.
+ *
+ * @return the values iterator
+ */
+ protected Iterator<V> createValuesIterator() {
+ return new ReferenceValuesIterator<K, V>(this);
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * EntrySet implementation.
+ */
+ static class ReferenceEntrySet <K,V> extends EntrySet<K, V> {
+
+ protected ReferenceEntrySet(AbstractHashedMap<K, V> parent) {
+ super(parent);
+ }
+
+ public Object[] toArray() {
+ return toArray(new Object[0]);
+ }
+
+ public <T> T[] toArray(T[] arr) {
+ // special implementation to handle disappearing entries
+ ArrayList<Map.Entry<K, V>> list = new ArrayList<Map.Entry<K, V>>();
+ Iterator<Map.Entry<K, V>> iterator = iterator();
+ while (iterator.hasNext()) {
+ Map.Entry<K, V> e = iterator.next();
+ list.add(new DefaultMapEntry<K, V>(e.getKey(), e.getValue()));
+ }
+ return list.toArray(arr);
+ }
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * KeySet implementation.
+ */
+ static class ReferenceKeySet <K,V> extends KeySet<K, V> {
+
+ protected ReferenceKeySet(AbstractHashedMap<K, V> parent) {
+ super(parent);
+ }
+
+ public Object[] toArray() {
+ return toArray(new Object[0]);
+ }
+
+ public <T> T[] toArray(T[] arr) {
+ // special implementation to handle disappearing keys
+ List<K> list = new ArrayList<K>(parent.size());
+ for (Iterator<K> it = iterator(); it.hasNext();) {
+ list.add(it.next());
+ }
+ return list.toArray(arr);
+ }
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Values implementation.
+ */
+ static class ReferenceValues <K,V> extends Values<K, V> {
+
+ protected ReferenceValues(AbstractHashedMap<K, V> parent) {
+ super(parent);
+ }
+
+ public Object[] toArray() {
+ return toArray(new Object[0]);
+ }
+
+ public <T> T[] toArray(T[] arr) {
+ // special implementation to handle disappearing values
+ List<V> list = new ArrayList<V>(parent.size());
+ for (Iterator<V> it = iterator(); it.hasNext();) {
+ list.add(it.next());
+ }
+ return list.toArray(arr);
+ }
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * A MapEntry implementation for the map.
+ * <p/>
+ * If getKey() or getValue() returns null, it means
+ * the mapping is stale and should be removed.
+ *
+ * @since Commons Collections 3.1
+ */
+ protected static class ReferenceEntry <K,V> extends HashEntry<K, V> {
+ /**
+ * The parent map
+ */
+ protected final AbstractReferenceMap<K, V> parent;
+
+ protected Reference<K> refKey;
+ protected Reference<V> refValue;
+
+ /**
+ * Creates a new entry object for the ReferenceMap.
+ *
+ * @param parent the parent map
+ * @param next the next entry in the hash bucket
+ * @param hashCode the hash code of the key
+ * @param key the key
+ * @param value the value
+ */
+ public ReferenceEntry(AbstractReferenceMap<K, V> parent, ReferenceEntry<K, V> next, int hashCode, K key, V value) {
+ super(next, hashCode, null, null);
+ this.parent = parent;
+ if (parent.keyType != HARD) {
+ refKey = toReference(parent.keyType, key, hashCode);
+ } else {
+ this.setKey(key);
+ }
+ if (parent.valueType != HARD) {
+ refValue = toReference(parent.valueType, value, hashCode); // the key hashCode is passed in deliberately
+ } else {
+ this.setValue(value);
+ }
+ }
+
+ /**
+ * Gets the key from the entry.
+ * This method dereferences weak and soft keys and thus may return null.
+ *
+ * @return the key, which may be null if it was garbage collected
+ */
+ public K getKey() {
+ return (parent.keyType > HARD) ? refKey.get() : super.getKey();
+ }
+
+ /**
+ * Gets the value from the entry.
+ * This method dereferences weak and soft value and thus may return null.
+ *
+ * @return the value, which may be null if it was garbage collected
+ */
+ public V getValue() {
+ return (parent.valueType > HARD) ? refValue.get() : super.getValue();
+ }
+
+ /**
+ * Sets the value of the entry.
+ *
+ * @param obj the object to store
+ * @return the previous value
+ */
+ public V setValue(V obj) {
+ V old = getValue();
+ if (parent.valueType > HARD) {
+ refValue.clear();
+ refValue = toReference(parent.valueType, obj, hashCode);
+ } else {
+ super.setValue(obj);
+ }
+ return old;
+ }
+
+ /**
+ * Compares this map entry to another.
+ * <p/>
+ * This implementation uses <code>isEqualKey</code> and
+ * <code>isEqualValue</code> on the main map for comparison.
+ *
+ * @param obj the other map entry to compare to
+ * @return true if equal, false if not
+ */
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj instanceof Map.Entry == false) {
+ return false;
+ }
+
+ Map.Entry entry = (Map.Entry) obj;
+ Object entryKey = entry.getKey(); // convert to hard reference
+ Object entryValue = entry.getValue(); // convert to hard reference
+ if ((entryKey == null) || (entryValue == null)) {
+ return false;
+ }
+ // compare using map methods, aiding identity subclass
+ // note that key is direct access and value is via method
+ return parent.isEqualKey(entryKey, getKey()) && parent.isEqualValue(entryValue, getValue());
+ }
+
+ /**
+ * Gets the hashcode of the entry using temporary hard references.
+ * <p/>
+ * This implementation uses <code>hashEntry</code> on the main map.
+ *
+ * @return the hashcode of the entry
+ */
+ public int hashCode() {
+ return parent.hashEntry(getKey(), getValue());
+ }
+
+ /**
+ * Constructs a reference of the given type to the given referent.
+ * The reference is registered with the queue for later purging.
+ *
+ * @param type HARD, SOFT or WEAK
+ * @param referent the object to refer to
+ * @param hash the hash code of the <i>key</i> of the mapping;
+ * this number might be different from referent.hashCode() if
+ * the referent represents a value and not a key
+ */
+ protected <T> Reference<T> toReference(int type, T referent, int hash) {
+ switch (type) {
+ case SOFT:
+ return new SoftRef<T>(hash, referent, parent.queue);
+ case WEAK:
+ return new WeakRef<T>(hash, referent, parent.queue);
+ default:
+ throw new Error("Attempt to create hard reference in ReferenceMap!");
+ }
+ }
+
+ /**
+ * Purges the specified reference
+ *
+ * @param ref the reference to purge
+ * @return true or false
+ */
+ boolean purge(Reference ref) {
+ boolean r = (parent.keyType > HARD) && (refKey == ref);
+ r = r || ((parent.valueType > HARD) && (refValue == ref));
+ if (r) {
+ if (parent.keyType > HARD) {
+ refKey.clear();
+ }
+ if (parent.valueType > HARD) {
+ refValue.clear();
+ } else if (parent.purgeValues) {
+ setValue(null);
+ }
+ }
+ return r;
+ }
+
+ /**
+ * Gets the next entry in the bucket.
+ *
+ * @return the next entry in the bucket
+ */
+ protected ReferenceEntry<K, V> next() {
+ return (ReferenceEntry<K, V>) next;
+ }
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * The EntrySet iterator.
+ */
+ static class ReferenceIteratorBase <K,V> {
+ /**
+ * The parent map
+ */
+ final AbstractReferenceMap<K, V> parent;
+
+ // These fields keep track of where we are in the table.
+ int index;
+ ReferenceEntry<K, V> entry;
+ ReferenceEntry<K, V> previous;
+
+ // These Object fields provide hard references to the
+ // current and next entry; this assures that if hasNext()
+ // returns true, next() will actually return a valid element.
+ K nextKey;
+ V nextValue;
+ K currentKey;
+ V currentValue;
+
+ int expectedModCount;
+
+ public ReferenceIteratorBase(AbstractReferenceMap<K, V> parent) {
+ super();
+ this.parent = parent;
+ index = (parent.size() != 0 ? parent.data.length : 0);
+ // have to do this here! size() invocation above
+ // may have altered the modCount.
+ expectedModCount = parent.modCount;
+ }
+
+ public boolean hasNext() {
+ checkMod();
+ while (nextNull()) {
+ ReferenceEntry<K, V> e = entry;
+ int i = index;
+ while ((e == null) && (i > 0)) {
+ i--;
+ e = (ReferenceEntry<K, V>) parent.data[i];
+ }
+ entry = e;
+ index = i;
+ if (e == null) {
+ currentKey = null;
+ currentValue = null;
+ return false;
+ }
+ nextKey = e.getKey();
+ nextValue = e.getValue();
+ if (nextNull()) {
+ entry = entry.next();
+ }
+ }
+ return true;
+ }
+
+ private void checkMod() {
+ if (parent.modCount != expectedModCount) {
+ throw new ConcurrentModificationException();
+ }
+ }
+
+ private boolean nextNull() {
+ return (nextKey == null) || (nextValue == null);
+ }
+
+ protected ReferenceEntry<K, V> nextEntry() {
+ checkMod();
+ if (nextNull() && !hasNext()) {
+ throw new NoSuchElementException();
+ }
+ previous = entry;
+ entry = entry.next();
+ currentKey = nextKey;
+ currentValue = nextValue;
+ nextKey = null;
+ nextValue = null;
+ return previous;
+ }
+
+ protected ReferenceEntry<K, V> currentEntry() {
+ checkMod();
+ return previous;
+ }
+
+ public ReferenceEntry<K, V> superNext() {
+ return nextEntry();
+ }
+
+ public void remove() {
+ checkMod();
+ if (previous == null) {
+ throw new IllegalStateException();
+ }
+ parent.remove(currentKey);
+ previous = null;
+ currentKey = null;
+ currentValue = null;
+ expectedModCount = parent.modCount;
+ }
+ }
+
+ /**
+ * The EntrySet iterator.
+ */
+ static class ReferenceEntrySetIterator <K,V> extends ReferenceIteratorBase<K, V> implements Iterator<Map.Entry<K, V>> {
+
+ public ReferenceEntrySetIterator(AbstractReferenceMap<K, V> abstractReferenceMap) {
+ super(abstractReferenceMap);
+ }
+
+ public ReferenceEntry<K, V> next() {
+ return superNext();
+ }
+
+ }
+
+ /**
+ * The keySet iterator.
+ */
+ static class ReferenceKeySetIterator <K,V> extends ReferenceIteratorBase<K, V> implements Iterator<K> {
+
+ ReferenceKeySetIterator(AbstractReferenceMap<K, V> parent) {
+ super(parent);
+ }
+
+ public K next() {
+ return nextEntry().getKey();
+ }
+ }
+
+ /**
+ * The values iterator.
+ */
+ static class ReferenceValuesIterator <K,V> extends ReferenceIteratorBase<K, V> implements Iterator<V> {
+
+ ReferenceValuesIterator(AbstractReferenceMap<K, V> parent) {
+ super(parent);
+ }
+
+ public V next() {
+ return nextEntry().getValue();
+ }
+ }
+
+ /**
+ * The MapIterator implementation.
+ */
+ static class ReferenceMapIterator <K,V> extends ReferenceIteratorBase<K, V> implements MapIterator<K, V> {
+
+ protected ReferenceMapIterator(AbstractReferenceMap<K, V> parent) {
+ super(parent);
+ }
+
+ public K next() {
+ return nextEntry().getKey();
+ }
+
+ public K getKey() {
+ HashEntry<K, V> current = currentEntry();
+ if (current == null) {
+ throw new IllegalStateException(AbstractHashedMap.GETKEY_INVALID);
+ }
+ return current.getKey();
+ }
+
+ public V getValue() {
+ HashEntry<K, V> current = currentEntry();
+ if (current == null) {
+ throw new IllegalStateException(AbstractHashedMap.GETVALUE_INVALID);
+ }
+ return current.getValue();
+ }
+
+ public V setValue(V value) {
+ HashEntry<K, V> current = currentEntry();
+ if (current == null) {
+ throw new IllegalStateException(AbstractHashedMap.SETVALUE_INVALID);
+ }
+ return current.setValue(value);
+ }
+ }
+
+ //-----------------------------------------------------------------------
+ // These two classes store the hashCode of the key of
+ // of the mapping, so that after they're dequeued a quick
+ // lookup of the bucket in the table can occur.
+
+ /**
+ * A soft reference holder.
+ */
+ static class SoftRef <T> extends SoftReference<T> {
+ /**
+ * the hashCode of the key (even if the reference points to a value)
+ */
+ private int hash;
+
+ public SoftRef(int hash, T r, ReferenceQueue q) {
+ super(r, q);
+ this.hash = hash;
+ }
+
+ public int hashCode() {
+ return hash;
+ }
+ }
+
+ /**
+ * A weak reference holder.
+ */
+ static class WeakRef <T> extends WeakReference<T> {
+ /**
+ * the hashCode of the key (even if the reference points to a value)
+ */
+ private int hash;
+
+ public WeakRef(int hash, T r, ReferenceQueue q) {
+ super(r, q);
+ this.hash = hash;
+ }
+
+ public int hashCode() {
+ return hash;
+ }
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Replaces the superclass method to store the state of this class.
+ * <p/>
+ * Serialization is not one of the JDK's nicest topics. Normal serialization will
+ * initialise the superclass before the subclass. Sometimes however, this isn't
+ * what you want, as in this case the <code>put()</code> method on read can be
+ * affected by subclass state.
+ * <p/>
+ * The solution adopted here is to serialize the state data of this class in
+ * this protected method. This method must be called by the
+ * <code>writeObject()</code> of the first serializable subclass.
+ * <p/>
+ * Subclasses may override if they have a specific field that must be present
+ * on read before this implementation will work. Generally, the read determines
+ * what must be serialized here, if anything.
+ *
+ * @param out the output stream
+ */
+ protected void doWriteObject(ObjectOutputStream out) throws IOException {
+ out.writeInt(keyType);
+ out.writeInt(valueType);
+ out.writeBoolean(purgeValues);
+ out.writeFloat(loadFactor);
+ out.writeInt(data.length);
+ for (MapIterator it = mapIterator(); it.hasNext();) {
+ out.writeObject(it.next());
+ out.writeObject(it.getValue());
+ }
+ out.writeObject(null); // null terminate map
+ // do not call super.doWriteObject() as code there doesn't work for reference map
+ }
+
+ /**
+ * Replaces the superclassm method to read the state of this class.
+ * <p/>
+ * Serialization is not one of the JDK's nicest topics. Normal serialization will
+ * initialise the superclass before the subclass. Sometimes however, this isn't
+ * what you want, as in this case the <code>put()</code> method on read can be
+ * affected by subclass state.
+ * <p/>
+ * The solution adopted here is to deserialize the state data of this class in
+ * this protected method. This method must be called by the
+ * <code>readObject()</code> of the first serializable subclass.
+ * <p/>
+ * Subclasses may override if the subclass has a specific field that must be present
+ * before <code>put()</code> or <code>calculateThreshold()</code> will work correctly.
+ *
+ * @param in the input stream
+ */
+ protected void doReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ this.keyType = in.readInt();
+ this.valueType = in.readInt();
+ this.purgeValues = in.readBoolean();
+ this.loadFactor = in.readFloat();
+ int capacity = in.readInt();
+ init();
+ data = new HashEntry[capacity];
+ while (true) {
+ K key = (K) in.readObject();
+ if (key == null) {
+ break;
+ }
+ V value = (V) in.readObject();
+ put(key, value);
+ }
+ threshold = calculateThreshold(data.length, loadFactor);
+ // do not call super.doReadObject() as code there doesn't work for reference map
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/util/collections/DefaultMapEntry.java b/src/org/jivesoftware/smack/util/collections/DefaultMapEntry.java
new file mode 100644
index 0000000..ef752d0
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/collections/DefaultMapEntry.java
@@ -0,0 +1,65 @@
+// GenericsNote: Converted.
+/*
+ * Copyright 2001-2004 The Apache Software Foundation
+ *
+ * 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.collections;
+
+
+import java.util.Map;
+
+/**
+ * A restricted implementation of {@link java.util.Map.Entry} that prevents
+ * the MapEntry contract from being broken.
+ *
+ * @author James Strachan
+ * @author Michael A. Smith
+ * @author Neil O'Toole
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $
+ * @since Commons Collections 3.0
+ */
+public final class DefaultMapEntry <K,V> extends AbstractMapEntry<K, V> {
+
+ /**
+ * Constructs a new entry with the specified key and given value.
+ *
+ * @param key the key for the entry, may be null
+ * @param value the value for the entry, may be null
+ */
+ public DefaultMapEntry(final K key, final V value) {
+ super(key, value);
+ }
+
+ /**
+ * Constructs a new entry from the specified KeyValue.
+ *
+ * @param pair the pair to copy, must not be null
+ * @throws NullPointerException if the entry is null
+ */
+ public DefaultMapEntry(final KeyValue<K, V> pair) {
+ super(pair.getKey(), pair.getValue());
+ }
+
+ /**
+ * Constructs a new entry from the specified MapEntry.
+ *
+ * @param entry the entry to copy, must not be null
+ * @throws NullPointerException if the entry is null
+ */
+ public DefaultMapEntry(final Map.Entry<K, V> entry) {
+ super(entry.getKey(), entry.getValue());
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/util/collections/EmptyIterator.java b/src/org/jivesoftware/smack/util/collections/EmptyIterator.java
new file mode 100644
index 0000000..6a8707f
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/collections/EmptyIterator.java
@@ -0,0 +1,58 @@
+// GenericsNote: Converted.
+/*
+ * Copyright 2004 The Apache Software Foundation
+ *
+ * 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.collections;
+
+import java.util.Iterator;
+
+/**
+ * Provides an implementation of an empty iterator.
+ * <p/>
+ * This class provides an implementation of an empty iterator.
+ * This class provides for binary compatability between Commons Collections
+ * 2.1.1 and 3.1 due to issues with <code>IteratorUtils</code>.
+ *
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:24 $
+ * @since Commons Collections 2.1.1 and 3.1
+ */
+public class EmptyIterator <E> extends AbstractEmptyIterator<E> implements ResettableIterator<E> {
+
+ /**
+ * Singleton instance of the iterator.
+ *
+ * @since Commons Collections 3.1
+ */
+ public static final ResettableIterator RESETTABLE_INSTANCE = new EmptyIterator();
+ /**
+ * Singleton instance of the iterator.
+ *
+ * @since Commons Collections 2.1.1 and 3.1
+ */
+ public static final Iterator INSTANCE = RESETTABLE_INSTANCE;
+
+ public static <T> Iterator<T> getInstance() {
+ return INSTANCE;
+ }
+
+ /**
+ * Constructor.
+ */
+ protected EmptyIterator() {
+ super();
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/util/collections/EmptyMapIterator.java b/src/org/jivesoftware/smack/util/collections/EmptyMapIterator.java
new file mode 100644
index 0000000..013f5ed
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/collections/EmptyMapIterator.java
@@ -0,0 +1,42 @@
+// GenericsNote: Converted.
+/*
+ * Copyright 2004 The Apache Software Foundation
+ *
+ * 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.collections;
+
+/**
+ * Provides an implementation of an empty map iterator.
+ *
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:24 $
+ * @since Commons Collections 3.1
+ */
+public class EmptyMapIterator extends AbstractEmptyIterator implements MapIterator, ResettableIterator {
+
+ /**
+ * Singleton instance of the iterator.
+ *
+ * @since Commons Collections 3.1
+ */
+ public static final MapIterator INSTANCE = new EmptyMapIterator();
+
+ /**
+ * Constructor.
+ */
+ protected EmptyMapIterator() {
+ super();
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/util/collections/IterableMap.java b/src/org/jivesoftware/smack/util/collections/IterableMap.java
new file mode 100644
index 0000000..251b587
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/collections/IterableMap.java
@@ -0,0 +1,61 @@
+// GenericsNote: Converted.
+/*
+ * Copyright 2003-2004 The Apache Software Foundation
+ *
+ * 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.collections;
+
+import java.util.Map;
+
+/**
+ * Defines a map that can be iterated directly without needing to create an entry set.
+ * <p/>
+ * A map iterator is an efficient way of iterating over maps.
+ * There is no need to access the entry set or cast to Map Entry objects.
+ * <pre>
+ * IterableMap map = new HashedMap();
+ * MapIterator it = map.mapIterator();
+ * while (it.hasNext()) {
+ * Object key = it.next();
+ * Object value = it.getValue();
+ * it.setValue("newValue");
+ * }
+ * </pre>
+ *
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:19 $
+ * @since Commons Collections 3.0
+ */
+public interface IterableMap <K,V> extends Map<K, V> {
+
+ /**
+ * Obtains a <code>MapIterator</code> over the map.
+ * <p/>
+ * A map iterator is an efficient way of iterating over maps.
+ * There is no need to access the entry set or cast to Map Entry objects.
+ * <pre>
+ * IterableMap map = new HashedMap();
+ * MapIterator it = map.mapIterator();
+ * while (it.hasNext()) {
+ * Object key = it.next();
+ * Object value = it.getValue();
+ * it.setValue("newValue");
+ * }
+ * </pre>
+ *
+ * @return a map iterator
+ */
+ MapIterator<K, V> mapIterator();
+
+}
diff --git a/src/org/jivesoftware/smack/util/collections/KeyValue.java b/src/org/jivesoftware/smack/util/collections/KeyValue.java
new file mode 100644
index 0000000..c73621d
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/collections/KeyValue.java
@@ -0,0 +1,46 @@
+// GenericsNote: Converted.
+/*
+ * Copyright 2003-2004 The Apache Software Foundation
+ *
+ * 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.collections;
+
+/**
+ * Defines a simple key value pair.
+ * <p/>
+ * A Map Entry has considerable additional semantics over and above a simple
+ * key-value pair. This interface defines the minimum key value, with just the
+ * two get methods.
+ *
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:19 $
+ * @since Commons Collections 3.0
+ */
+public interface KeyValue <K,V> {
+
+ /**
+ * Gets the key from the pair.
+ *
+ * @return the key
+ */
+ K getKey();
+
+ /**
+ * Gets the value from the pair.
+ *
+ * @return the value
+ */
+ V getValue();
+
+}
diff --git a/src/org/jivesoftware/smack/util/collections/MapIterator.java b/src/org/jivesoftware/smack/util/collections/MapIterator.java
new file mode 100644
index 0000000..fe2398c
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/collections/MapIterator.java
@@ -0,0 +1,109 @@
+// GenericsNote: Converted.
+/*
+ * Copyright 2003-2004 The Apache Software Foundation
+ *
+ * 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.collections;
+
+import java.util.Iterator;
+
+/**
+ * Defines an iterator that operates over a <code>Map</code>.
+ * <p/>
+ * This iterator is a special version designed for maps. It can be more
+ * efficient to use this rather than an entry set iterator where the option
+ * is available, and it is certainly more convenient.
+ * <p/>
+ * A map that provides this interface may not hold the data internally using
+ * Map Entry objects, thus this interface can avoid lots of object creation.
+ * <p/>
+ * In use, this iterator iterates through the keys in the map. After each call
+ * to <code>next()</code>, the <code>getValue()</code> method provides direct
+ * access to the value. The value can also be set using <code>setValue()</code>.
+ * <pre>
+ * MapIterator it = map.mapIterator();
+ * while (it.hasNext()) {
+ * Object key = it.next();
+ * Object value = it.getValue();
+ * it.setValue(newValue);
+ * }
+ * </pre>
+ *
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:19 $
+ * @since Commons Collections 3.0
+ */
+public interface MapIterator <K,V> extends Iterator<K> {
+
+ /**
+ * Checks to see if there are more entries still to be iterated.
+ *
+ * @return <code>true</code> if the iterator has more elements
+ */
+ boolean hasNext();
+
+ /**
+ * Gets the next <em>key</em> from the <code>Map</code>.
+ *
+ * @return the next key in the iteration
+ * @throws java.util.NoSuchElementException
+ * if the iteration is finished
+ */
+ K next();
+
+ //-----------------------------------------------------------------------
+ /**
+ * Gets the current key, which is the key returned by the last call
+ * to <code>next()</code>.
+ *
+ * @return the current key
+ * @throws IllegalStateException if <code>next()</code> has not yet been called
+ */
+ K getKey();
+
+ /**
+ * Gets the current value, which is the value associated with the last key
+ * returned by <code>next()</code>.
+ *
+ * @return the current value
+ * @throws IllegalStateException if <code>next()</code> has not yet been called
+ */
+ V getValue();
+
+ //-----------------------------------------------------------------------
+ /**
+ * Removes the last returned key from the underlying <code>Map</code> (optional operation).
+ * <p/>
+ * This method can be called once per call to <code>next()</code>.
+ *
+ * @throws UnsupportedOperationException if remove is not supported by the map
+ * @throws IllegalStateException if <code>next()</code> has not yet been called
+ * @throws IllegalStateException if <code>remove()</code> has already been called
+ * since the last call to <code>next()</code>
+ */
+ void remove();
+
+ /**
+ * Sets the value associated with the current key (optional operation).
+ *
+ * @param value the new value
+ * @return the previous value
+ * @throws UnsupportedOperationException if setValue is not supported by the map
+ * @throws IllegalStateException if <code>next()</code> has not yet been called
+ * @throws IllegalStateException if <code>remove()</code> has been called since the
+ * last call to <code>next()</code>
+ */
+ V setValue(V value);
+
+}
diff --git a/src/org/jivesoftware/smack/util/collections/ReferenceMap.java b/src/org/jivesoftware/smack/util/collections/ReferenceMap.java
new file mode 100644
index 0000000..f30954d
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/collections/ReferenceMap.java
@@ -0,0 +1,161 @@
+// GenericsNote: Converted.
+/*
+ * Copyright 2002-2004 The Apache Software Foundation
+ *
+ * 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.collections;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+
+/**
+ * A <code>Map</code> implementation that allows mappings to be
+ * removed by the garbage collector.
+ * <p/>
+ * When you construct a <code>ReferenceMap</code>, you can specify what kind
+ * of references are used to store the map's keys and values.
+ * If non-hard references are used, then the garbage collector can remove
+ * mappings if a key or value becomes unreachable, or if the JVM's memory is
+ * running low. For information on how the different reference types behave,
+ * see {@link java.lang.ref.Reference}.
+ * <p/>
+ * Different types of references can be specified for keys and values.
+ * The keys can be configured to be weak but the values hard,
+ * in which case this class will behave like a
+ * <a href="http://java.sun.com/j2se/1.4/docs/api/java/util/WeakHashMap.html">
+ * <code>WeakHashMap</code></a>. However, you can also specify hard keys and
+ * weak values, or any other combination. The default constructor uses
+ * hard keys and soft values, providing a memory-sensitive cache.
+ * <p/>
+ * This map is similar to ReferenceIdentityMap.
+ * It differs in that keys and values in this class are compared using <code>equals()</code>.
+ * <p/>
+ * This {@link java.util.Map} implementation does <i>not</i> allow null elements.
+ * Attempting to add a null key or value to the map will raise a <code>NullPointerException</code>.
+ * <p/>
+ * This implementation is not synchronized.
+ * You can use {@link java.util.Collections#synchronizedMap} to
+ * provide synchronized access to a <code>ReferenceMap</code>.
+ * Remember that synchronization will not stop the garbage collecter removing entries.
+ * <p/>
+ * All the available iterators can be reset back to the start by casting to
+ * <code>ResettableIterator</code> and calling <code>reset()</code>.
+ * <p/>
+ * NOTE: As from Commons Collections 3.1 this map extends <code>AbstractReferenceMap</code>
+ * (previously it extended AbstractMap). As a result, the implementation is now
+ * extensible and provides a <code>MapIterator</code>.
+ *
+ * @author Paul Jack
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $
+ * @see java.lang.ref.Reference
+ * @since Commons Collections 3.0 (previously in main package v2.1)
+ */
+public class ReferenceMap <K,V> extends AbstractReferenceMap<K, V> implements Serializable {
+
+ /**
+ * Serialization version
+ */
+ private static final long serialVersionUID = 1555089888138299607L;
+
+ /**
+ * Constructs a new <code>ReferenceMap</code> that will
+ * use hard references to keys and soft references to values.
+ */
+ public ReferenceMap() {
+ super(HARD, SOFT, DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, false);
+ }
+
+ /**
+ * Constructs a new <code>ReferenceMap</code> that will
+ * use the specified types of references.
+ *
+ * @param keyType the type of reference to use for keys;
+ * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK}
+ * @param valueType the type of reference to use for values;
+ * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK}
+ */
+ public ReferenceMap(int keyType, int valueType) {
+ super(keyType, valueType, DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, false);
+ }
+
+ /**
+ * Constructs a new <code>ReferenceMap</code> that will
+ * use the specified types of references.
+ *
+ * @param keyType the type of reference to use for keys;
+ * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK}
+ * @param valueType the type of reference to use for values;
+ * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK}
+ * @param purgeValues should the value be automatically purged when the
+ * key is garbage collected
+ */
+ public ReferenceMap(int keyType, int valueType, boolean purgeValues) {
+ super(keyType, valueType, DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, purgeValues);
+ }
+
+ /**
+ * Constructs a new <code>ReferenceMap</code> with the
+ * specified reference types, load factor and initial
+ * capacity.
+ *
+ * @param keyType the type of reference to use for keys;
+ * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK}
+ * @param valueType the type of reference to use for values;
+ * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK}
+ * @param capacity the initial capacity for the map
+ * @param loadFactor the load factor for the map
+ */
+ public ReferenceMap(int keyType, int valueType, int capacity, float loadFactor) {
+ super(keyType, valueType, capacity, loadFactor, false);
+ }
+
+ /**
+ * Constructs a new <code>ReferenceMap</code> with the
+ * specified reference types, load factor and initial
+ * capacity.
+ *
+ * @param keyType the type of reference to use for keys;
+ * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK}
+ * @param valueType the type of reference to use for values;
+ * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK}
+ * @param capacity the initial capacity for the map
+ * @param loadFactor the load factor for the map
+ * @param purgeValues should the value be automatically purged when the
+ * key is garbage collected
+ */
+ public ReferenceMap(int keyType, int valueType, int capacity, float loadFactor, boolean purgeValues) {
+ super(keyType, valueType, capacity, loadFactor, purgeValues);
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Write the map out using a custom routine.
+ */
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ doWriteObject(out);
+ }
+
+ /**
+ * Read the map in using a custom routine.
+ */
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+ doReadObject(in);
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/util/collections/ResettableIterator.java b/src/org/jivesoftware/smack/util/collections/ResettableIterator.java
new file mode 100644
index 0000000..cf814f7
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/collections/ResettableIterator.java
@@ -0,0 +1,38 @@
+// GenericsNote: Converted.
+/*
+ * Copyright 2003-2004 The Apache Software Foundation
+ *
+ * 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.collections;
+
+import java.util.Iterator;
+
+/**
+ * Defines an iterator that can be reset back to an initial state.
+ * <p/>
+ * This interface allows an iterator to be repeatedly reused.
+ *
+ * @author Matt Hall, John Watkinson, Stephen Colebourne
+ * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:19 $
+ * @since Commons Collections 3.0
+ */
+public interface ResettableIterator <E> extends Iterator<E> {
+
+ /**
+ * Resets the iterator back to the position at which the iterator
+ * was created.
+ */
+ public void reset();
+
+}
diff --git a/src/org/jivesoftware/smack/util/dns/DNSJavaResolver.java b/src/org/jivesoftware/smack/util/dns/DNSJavaResolver.java
new file mode 100644
index 0000000..dd93fd3
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/dns/DNSJavaResolver.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright 2013 Florian Schmaus
+ *
+ * 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.dns;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.xbill.DNS.Lookup;
+import org.xbill.DNS.Record;
+import org.xbill.DNS.Type;
+
+/**
+ * This implementation uses the <a href="http://www.dnsjava.org/">dnsjava</a> implementation for resolving DNS addresses.
+ *
+ */
+public class DNSJavaResolver implements DNSResolver {
+
+ private static DNSJavaResolver instance = new DNSJavaResolver();
+
+ private DNSJavaResolver() {
+
+ }
+
+ public static DNSResolver getInstance() {
+ return instance;
+ }
+
+ @Override
+ public List<SRVRecord> lookupSRVRecords(String name) {
+ List<SRVRecord> res = new ArrayList<SRVRecord>();
+
+ try {
+ Lookup lookup = new Lookup(name, Type.SRV);
+ Record recs[] = lookup.run();
+ if (recs == null)
+ return res;
+
+ for (Record record : recs) {
+ org.xbill.DNS.SRVRecord srvRecord = (org.xbill.DNS.SRVRecord) record;
+ if (srvRecord != null && srvRecord.getTarget() != null) {
+ String host = srvRecord.getTarget().toString();
+ int port = srvRecord.getPort();
+ int priority = srvRecord.getPriority();
+ int weight = srvRecord.getWeight();
+
+ SRVRecord r;
+ try {
+ r = new SRVRecord(host, port, priority, weight);
+ } catch (Exception e) {
+ continue;
+ }
+ res.add(r);
+ }
+ }
+
+ } catch (Exception e) {
+ }
+ return res;
+ }
+}
diff --git a/src/org/jivesoftware/smack/util/dns/DNSResolver.java b/src/org/jivesoftware/smack/util/dns/DNSResolver.java
new file mode 100644
index 0000000..86f037b
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/dns/DNSResolver.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2013 Florian Schmaus
+ *
+ * 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.dns;
+
+import java.util.List;
+
+/**
+ * Implementations of this interface define a class that is capable of resolving DNS addresses.
+ *
+ */
+public interface DNSResolver {
+
+ /**
+ * Gets a list of service records for the specified service.
+ * @param name The symbolic name of the service.
+ * @return The list of SRV records mapped to the service name.
+ */
+ List<SRVRecord> lookupSRVRecords(String name);
+
+}
diff --git a/src/org/jivesoftware/smack/util/dns/HostAddress.java b/src/org/jivesoftware/smack/util/dns/HostAddress.java
new file mode 100644
index 0000000..eb8b07a
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/dns/HostAddress.java
@@ -0,0 +1,109 @@
+/**
+ * Copyright 2013 Florian Schmaus
+ *
+ * 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.dns;
+
+public class HostAddress {
+ private String fqdn;
+ private int port;
+ private Exception exception;
+
+ /**
+ * Creates a new HostAddress with the given FQDN. The port will be set to the default XMPP client port: 5222
+ *
+ * @param fqdn Fully qualified domain name.
+ * @throws IllegalArgumentException If the fqdn is null.
+ */
+ public HostAddress(String fqdn) {
+ if (fqdn == null)
+ throw new IllegalArgumentException("FQDN is null");
+ if (fqdn.charAt(fqdn.length() - 1) == '.') {
+ this.fqdn = fqdn.substring(0, fqdn.length() - 1);
+ }
+ else {
+ this.fqdn = fqdn;
+ }
+ // Set port to the default port for XMPP client communication
+ this.port = 5222;
+ }
+
+ /**
+ * Creates a new HostAddress with the given FQDN. The port will be set to the default XMPP client port: 5222
+ *
+ * @param fqdn Fully qualified domain name.
+ * @param port The port to connect on.
+ * @throws IllegalArgumentException If the fqdn is null or port is out of valid range (0 - 65535).
+ */
+ public HostAddress(String fqdn, int port) {
+ this(fqdn);
+ if (port < 0 || port > 65535)
+ throw new IllegalArgumentException(
+ "DNS SRV records weight must be a 16-bit unsiged integer (i.e. between 0-65535. Port was: " + port);
+
+ this.port = port;
+ }
+
+ public String getFQDN() {
+ return fqdn;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setException(Exception e) {
+ this.exception = e;
+ }
+
+ @Override
+ public String toString() {
+ return fqdn + ":" + port;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof HostAddress)) {
+ return false;
+ }
+
+ final HostAddress address = (HostAddress) o;
+
+ if (!fqdn.equals(address.fqdn)) {
+ return false;
+ }
+ return port == address.port;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 37 * result + fqdn.hashCode();
+ return result * 37 + port;
+ }
+
+ public String getErrorMessage() {
+ String error;
+ if (exception == null) {
+ error = "No error logged";
+ }
+ else {
+ error = exception.getMessage();
+ }
+ return toString() + " Exception: " + error;
+ }
+}
diff --git a/src/org/jivesoftware/smack/util/dns/SRVRecord.java b/src/org/jivesoftware/smack/util/dns/SRVRecord.java
new file mode 100644
index 0000000..457e40e
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/dns/SRVRecord.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright 2013 Florian Schmaus
+ *
+ * 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.dns;
+
+/**
+ * @see <a href="http://tools.ietf.org/html/rfc2782>RFC 2782: A DNS RR for specifying the location of services (DNS
+ * SRV)<a>
+ * @author Florian Schmaus
+ *
+ */
+public class SRVRecord extends HostAddress implements Comparable<SRVRecord> {
+
+ private int weight;
+ private int priority;
+
+ /**
+ * Create a new SRVRecord
+ *
+ * @param fqdn Fully qualified domain name
+ * @param port The connection port
+ * @param priority Priority of the target host
+ * @param weight Relative weight for records with same priority
+ * @throws IllegalArgumentException fqdn is null or any other field is not in valid range (0-65535).
+ */
+ public SRVRecord(String fqdn, int port, int priority, int weight) {
+ super(fqdn, port);
+ if (weight < 0 || weight > 65535)
+ throw new IllegalArgumentException(
+ "DNS SRV records weight must be a 16-bit unsiged integer (i.e. between 0-65535. Weight was: "
+ + weight);
+
+ if (priority < 0 || priority > 65535)
+ throw new IllegalArgumentException(
+ "DNS SRV records priority must be a 16-bit unsiged integer (i.e. between 0-65535. Priority was: "
+ + priority);
+
+ this.priority = priority;
+ this.weight = weight;
+
+ }
+
+ public int getPriority() {
+ return priority;
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+
+ @Override
+ public int compareTo(SRVRecord other) {
+ // According to RFC2782,
+ // "[a] client MUST attempt to contact the target host with the lowest-numbered priority it can reach".
+ // This means that a SRV record with a higher priority is 'less' then one with a lower.
+ int res = other.priority - this.priority;
+ if (res == 0) {
+ res = this.weight - other.weight;
+ }
+ return res;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + " prio:" + priority + ":w:" + weight;
+ }
+}
diff --git a/src/org/jivesoftware/smack/util/package.html b/src/org/jivesoftware/smack/util/package.html
new file mode 100644
index 0000000..e34bfe3
--- /dev/null
+++ b/src/org/jivesoftware/smack/util/package.html
@@ -0,0 +1 @@
+<body>Utility classes.</body> \ No newline at end of file