diff options
Diffstat (limited to 'src/org/jivesoftware/smack')
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> + * <foo xmlns="http://bar.com"> + * <color>blue</color> + * <food>pizza</food> + * </foo></pre> + * + * In this case, getValue("color") would return "blue", and getValue("food") would + * return "pizza". This parsing mechanism mechanism is very simplistic and will not work + * as desired in all cases (for example, if some of the elements have attributes. In those + * cases, a custom PacketExtensionProvider should be used. + * + * @author Matt Tucker + */ +public class DefaultPacketExtension implements PacketExtension { + + private String elementName; + private String namespace; + private Map<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><query xmlns="jabber:iq:auth"> -- an authentication IQ. + * <li><query xmlns="jabber:iq:private"> -- a private storage IQ. + * <li><pubsub xmlns="http://jabber.org/protocol/pubsub"> -- a pubsub IQ. + * </ul> + * + * @author Matt Tucker + */ +public abstract class IQ extends Packet { + + private Type type = Type.GET; + + 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> </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
+ *
+ * <message from='pubsub.shakespeare.lit' to='francisco@denmark.lit' id='foo>
+ * <event xmlns='http://jabber.org/protocol/pubsub#event>
+ * <items node='princely_musings'>
+ * <item id='asdjkwei3i34234n356'>
+ * <entry xmlns='http://www.w3.org/2005/Atom'>
+ * <title>Soliloquy</title>
+ * <link rel='alternative' type='text/html'/>
+ * <id>tag:denmark.lit,2003:entry-32397</id>
+ * </entry>
+ * </item>
+ * </items>
+ * </event>
+ * </message>
+ *
+ * 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.
+ *
+ * <extensionProvider>
+ * <elementName>items</elementName>
+ * <namespace>http://jabber.org/protocol/pubsub#event</namespace>
+ * <className>org.jivesoftware.smackx.provider.ItemsEventProvider</className>
+ * </extensionProvider>
+ * <extensionProvider>
+ * <elementName>item</elementName>
+ * <namespace>http://jabber.org/protocol/pubsub#event</namespace>
+ * <className>org.jivesoftware.smackx.provider.ItemProvider</className>
+ * </extensionProvider>
+ *
+ * </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> + * <?xml version="1.0"?> + * <smackProviders> + * <iqProvider> + * <elementName>query</elementName> + * <namespace>jabber:iq:time</namespace> + * <className>org.jivesoftware.smack.packet.Time</className> + * </iqProvider> + * </smackProviders></pre> + * + * Each IQ provider is associated with an element name and a namespace. If multiple provider + * entries attempt to register to handle the same namespace, the first entry loaded from the + * classpath will take precedence. The IQ provider class can either implement the IQProvider + * interface, or extend the IQ class. In the former case, each IQProvider is responsible for + * parsing the raw XML stream to create an IQ instance. In the latter case, bean introspection + * is used to try to automatically set properties of the IQ instance using the values found + * in the IQ packet XML. For example, an XMPP time packet resembles the following: + * <pre> + * <iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'> + * <query xmlns='jabber:iq:time'> + * <utc>20020910T17:58:35</utc> + * <tz>MDT</tz> + * <display>Tue Sep 10 12:58:35 2002</display> + * </query> + * </iq></pre> + * + * In order for this packet to be automatically mapped to the Time object listed in the + * providers file above, it must have the methods setUtc(String), setTz(String), and + * setDisplay(String). The introspection service will automatically try to convert the String + * value from the XML into a boolean, int, long, float, double, or Class depending on the + * type the IQ instance expects.<p> + * + * A pluggable system for packet extensions, child elements in a custom namespace for + * message and presence packets, also exists. Each extension provider + * is registered with a name space in the smack.providers file as in the following example: + * + * <pre> + * <?xml version="1.0"?> + * <smackProviders> + * <extensionProvider> + * <elementName>x</elementName> + * <namespace>jabber:iq:event</namespace> + * <className>org.jivesoftware.smack.packet.MessageEvent</className> + * </extensionProvider> + * </smackProviders></pre> + * + * If multiple provider entries attempt to register to handle the same element name and namespace, + * the first entry loaded from the classpath will take precedence. Whenever a packet extension + * is found in a packet, parsing will be passed to the correct provider. Each provider + * can either implement the PacketExtensionProvider interface or be a standard Java Bean. In + * the former case, each extension provider is responsible for parsing the raw XML stream to + * contruct an object. In the latter case, bean introspection is used to try to automatically + * set the properties of the class using the values in the packet extension sub-element. When an + * extension provider is not registered for an element name and namespace combination, Smack will + * store all top-level elements of the sub-packet in DefaultPacketExtension object and then + * attach it to the packet.<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> + * <iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'> + * <query xmlns='jabber:iq:time'> + * <utc>20020910T17:58:35</utc> + * <tz>MDT</tz> + * <display>Tue Sep 10 12:58:35 2002</display> + * </query> + * </iq></pre> + * + * <p>Note: this method is generally only called by the internal Smack classes. + * + * @param elementName the XML element name. + * @param namespace the XML namespace. + * @return the IQ provider. + */ + public Object getIQProvider(String elementName, String namespace) { + String key = getProviderKey(elementName, namespace); + return iqProviders.get(key); + } + + /** + * Returns an 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> + * <message to='romeo@montague.net' id='message_1'> + * <body>Art thou not Romeo, and a Montague?</body> + * <x xmlns='jabber:x:event'> + * <composing/> + * </x> + * </message></pre> + * + * <p>Note: this method is generally only called by the internal Smack classes. + * + * @param elementName 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 = """.toCharArray(); + private static final char[] APOS_ENCODE = "'".toCharArray(); + private static final char[] AMP_ENCODE = "&".toCharArray(); + private static final char[] LT_ENCODE = "<".toCharArray(); + private static final char[] GT_ENCODE = ">".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><space></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><</td><td>\3c</td></tr> + * <tr><td>></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 <space> 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><space></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><</td><td>\3c</td></tr> + * <tr><td>></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 <space> 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 ë (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 > 0.0f and generally < 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 |