diff options
Diffstat (limited to 'src/org/jivesoftware')
434 files changed, 78438 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 diff --git a/src/org/jivesoftware/smackx/ChatState.java b/src/org/jivesoftware/smackx/ChatState.java new file mode 100644 index 0000000..4acaa49 --- /dev/null +++ b/src/org/jivesoftware/smackx/ChatState.java @@ -0,0 +1,50 @@ +/** + * $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.smackx; + +/** + * Represents the current state of a users interaction with another user. Implemented according to + * <a href="http://www.xmpp.org/extensions/xep-0085.html">XEP-0085</a>. + * + * @author Alexander Wenckus + */ +public enum ChatState { + /** + * User is actively participating in the chat session. + */ + active, + /** + * User is composing a message. + */ + composing, + /** + * User had been composing but now has stopped. + */ + paused, + /** + * User has not been actively participating in the chat session. + */ + inactive, + /** + * User has effectively ended their participation in the chat session. + */ + gone +} diff --git a/src/org/jivesoftware/smackx/ChatStateListener.java b/src/org/jivesoftware/smackx/ChatStateListener.java new file mode 100644 index 0000000..9a1bf79 --- /dev/null +++ b/src/org/jivesoftware/smackx/ChatStateListener.java @@ -0,0 +1,40 @@ +/** + * $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.smackx; + +import org.jivesoftware.smack.Chat; +import org.jivesoftware.smack.MessageListener; + +/** + * Events for when the state of a user in a chat changes. + * + * @author Alexander Wenckus + */ +public interface ChatStateListener extends MessageListener { + + /** + * Fired when the state of a chat with another user changes. + * + * @param chat the chat in which the state has changed. + * @param state the new state of the participant. + */ + void stateChanged(Chat chat, ChatState state); +} diff --git a/src/org/jivesoftware/smackx/ChatStateManager.java b/src/org/jivesoftware/smackx/ChatStateManager.java new file mode 100644 index 0000000..d452a9f --- /dev/null +++ b/src/org/jivesoftware/smackx/ChatStateManager.java @@ -0,0 +1,200 @@ +/** + * $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.smackx; + +import org.jivesoftware.smack.*; +import org.jivesoftware.smack.util.collections.ReferenceMap; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.NotFilter; +import org.jivesoftware.smack.filter.PacketExtensionFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.packet.ChatStateExtension; + +import java.util.Map; +import java.util.WeakHashMap; + +/** + * Handles chat state for all chats on a particular Connection. This class manages both the + * packet extensions and the disco response neccesary for compliance with + * <a href="http://www.xmpp.org/extensions/xep-0085.html">XEP-0085</a>. + * + * NOTE: {@link org.jivesoftware.smackx.ChatStateManager#getInstance(org.jivesoftware.smack.Connection)} + * needs to be called in order for the listeners to be registered appropriately with the connection. + * If this does not occur you will not receive the update notifications. + * + * @author Alexander Wenckus + * @see org.jivesoftware.smackx.ChatState + * @see org.jivesoftware.smackx.packet.ChatStateExtension + */ +public class ChatStateManager { + + private static final Map<Connection, ChatStateManager> managers = + new WeakHashMap<Connection, ChatStateManager>(); + + private static final PacketFilter filter = new NotFilter( + new PacketExtensionFilter("http://jabber.org/protocol/chatstates")); + + /** + * Returns the ChatStateManager related to the Connection and it will create one if it does + * not yet exist. + * + * @param connection the connection to return the ChatStateManager + * @return the ChatStateManager related the the connection. + */ + public static ChatStateManager getInstance(final Connection connection) { + if(connection == null) { + return null; + } + synchronized (managers) { + ChatStateManager manager = managers.get(connection); + if (manager == null) { + manager = new ChatStateManager(connection); + manager.init(); + managers.put(connection, manager); + } + + return manager; + } + } + + private final Connection connection; + + private final OutgoingMessageInterceptor outgoingInterceptor = new OutgoingMessageInterceptor(); + + private final IncomingMessageInterceptor incomingInterceptor = new IncomingMessageInterceptor(); + + /** + * Maps chat to last chat state. + */ + private final Map<Chat, ChatState> chatStates = + new ReferenceMap<Chat, ChatState>(ReferenceMap.WEAK, ReferenceMap.HARD); + + private ChatStateManager(Connection connection) { + this.connection = connection; + } + + private void init() { + connection.getChatManager().addOutgoingMessageInterceptor(outgoingInterceptor, + filter); + connection.getChatManager().addChatListener(incomingInterceptor); + + ServiceDiscoveryManager.getInstanceFor(connection) + .addFeature("http://jabber.org/protocol/chatstates"); + } + + /** + * Sets the current state of the provided chat. This method will send an empty bodied Message + * packet with the state attached as a {@link org.jivesoftware.smack.packet.PacketExtension}, if + * and only if the new chat state is different than the last state. + * + * @param newState the new state of the chat + * @param chat the chat. + * @throws org.jivesoftware.smack.XMPPException + * when there is an error sending the message + * packet. + */ + public void setCurrentState(ChatState newState, Chat chat) throws XMPPException { + if(chat == null || newState == null) { + throw new IllegalArgumentException("Arguments cannot be null."); + } + if(!updateChatState(chat, newState)) { + return; + } + Message message = new Message(); + ChatStateExtension extension = new ChatStateExtension(newState); + message.addExtension(extension); + + chat.sendMessage(message); + } + + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ChatStateManager that = (ChatStateManager) o; + + return connection.equals(that.connection); + + } + + public int hashCode() { + return connection.hashCode(); + } + + private boolean updateChatState(Chat chat, ChatState newState) { + ChatState lastChatState = chatStates.get(chat); + if (lastChatState != newState) { + chatStates.put(chat, newState); + return true; + } + return false; + } + + private void fireNewChatState(Chat chat, ChatState state) { + for (MessageListener listener : chat.getListeners()) { + if (listener instanceof ChatStateListener) { + ((ChatStateListener) listener).stateChanged(chat, state); + } + } + } + + private class OutgoingMessageInterceptor implements PacketInterceptor { + + public void interceptPacket(Packet packet) { + Message message = (Message) packet; + Chat chat = connection.getChatManager().getThreadChat(message.getThread()); + if (chat == null) { + return; + } + if (updateChatState(chat, ChatState.active)) { + message.addExtension(new ChatStateExtension(ChatState.active)); + } + } + } + + private class IncomingMessageInterceptor implements ChatManagerListener, MessageListener { + + public void chatCreated(final Chat chat, boolean createdLocally) { + chat.addMessageListener(this); + } + + public void processMessage(Chat chat, Message message) { + PacketExtension extension + = message.getExtension("http://jabber.org/protocol/chatstates"); + if (extension == null) { + return; + } + + ChatState state; + try { + state = ChatState.valueOf(extension.getElementName()); + } + catch (Exception ex) { + return; + } + + fireNewChatState(chat, state); + } + } +} diff --git a/src/org/jivesoftware/smackx/ConfigureProviderManager.java b/src/org/jivesoftware/smackx/ConfigureProviderManager.java new file mode 100644 index 0000000..7c0cdf2 --- /dev/null +++ b/src/org/jivesoftware/smackx/ConfigureProviderManager.java @@ -0,0 +1,207 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx; + +import org.jivesoftware.smack.provider.PrivacyProvider; +import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smackx.GroupChatInvitation; +import org.jivesoftware.smackx.PrivateDataManager; +import org.jivesoftware.smackx.bytestreams.ibb.provider.CloseIQProvider; +import org.jivesoftware.smackx.bytestreams.ibb.provider.DataPacketProvider; +import org.jivesoftware.smackx.bytestreams.ibb.provider.OpenIQProvider; +import org.jivesoftware.smackx.bytestreams.socks5.provider.BytestreamsProvider; +import org.jivesoftware.smackx.carbons.Carbon; +import org.jivesoftware.smackx.entitycaps.provider.CapsExtensionProvider; +import org.jivesoftware.smackx.forward.Forwarded; +import org.jivesoftware.smackx.packet.AttentionExtension; +import org.jivesoftware.smackx.packet.ChatStateExtension; +import org.jivesoftware.smackx.packet.LastActivity; +import org.jivesoftware.smackx.packet.Nick; +import org.jivesoftware.smackx.packet.OfflineMessageInfo; +import org.jivesoftware.smackx.packet.OfflineMessageRequest; +import org.jivesoftware.smackx.packet.SharedGroupsInfo; +import org.jivesoftware.smackx.ping.provider.PingProvider; +import org.jivesoftware.smackx.provider.DataFormProvider; +import org.jivesoftware.smackx.provider.DelayInformationProvider; +import org.jivesoftware.smackx.provider.DiscoverInfoProvider; +import org.jivesoftware.smackx.provider.DiscoverItemsProvider; +import org.jivesoftware.smackx.provider.HeadersProvider; +import org.jivesoftware.smackx.provider.HeaderProvider; +import org.jivesoftware.smackx.provider.MUCAdminProvider; +import org.jivesoftware.smackx.provider.MUCOwnerProvider; +import org.jivesoftware.smackx.provider.MUCUserProvider; +import org.jivesoftware.smackx.provider.MessageEventProvider; +import org.jivesoftware.smackx.provider.MultipleAddressesProvider; +import org.jivesoftware.smackx.provider.RosterExchangeProvider; +import org.jivesoftware.smackx.provider.StreamInitiationProvider; +import org.jivesoftware.smackx.provider.VCardProvider; +import org.jivesoftware.smackx.provider.XHTMLExtensionProvider; +import org.jivesoftware.smackx.pubsub.provider.AffiliationProvider; +import org.jivesoftware.smackx.pubsub.provider.AffiliationsProvider; +import org.jivesoftware.smackx.pubsub.provider.ConfigEventProvider; +import org.jivesoftware.smackx.pubsub.provider.EventProvider; +import org.jivesoftware.smackx.pubsub.provider.FormNodeProvider; +import org.jivesoftware.smackx.pubsub.provider.ItemProvider; +import org.jivesoftware.smackx.pubsub.provider.ItemsProvider; +import org.jivesoftware.smackx.pubsub.provider.PubSubProvider; +import org.jivesoftware.smackx.pubsub.provider.RetractEventProvider; +import org.jivesoftware.smackx.pubsub.provider.SimpleNodeProvider; +import org.jivesoftware.smackx.pubsub.provider.SubscriptionProvider; +import org.jivesoftware.smackx.pubsub.provider.SubscriptionsProvider; +import org.jivesoftware.smackx.receipts.DeliveryReceipt; +import org.jivesoftware.smackx.search.UserSearch; + +/** + * Since dalvik on Android does not allow the loading of META-INF files from the + * filesystem, you have to register every provider manually. + * + * The full list of providers is at: + * http://fisheye.igniterealtime.org/browse/smack/trunk/build/resources/META-INF/smack.providers?hb=true + * + * @author Florian Schmaus fschmaus@gmail.com + * + */ +public class ConfigureProviderManager { + + public static void configureProviderManager() { + ProviderManager pm = ProviderManager.getInstance(); + + // The order is the same as in the smack.providers file + + // Private Data Storage + pm.addIQProvider("query","jabber:iq:private", new PrivateDataManager.PrivateDataIQProvider()); + // Time + try { + pm.addIQProvider("query","jabber:iq:time", Class.forName("org.jivesoftware.smackx.packet.Time")); + } catch (ClassNotFoundException e) { + System.err.println("Can't load class for org.jivesoftware.smackx.packet.Time"); + } + + // Roster Exchange + pm.addExtensionProvider("x","jabber:x:roster", new RosterExchangeProvider()); + // Message Events + pm.addExtensionProvider("x","jabber:x:event", new MessageEventProvider()); + // Chat State + pm.addExtensionProvider("active","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider()); + pm.addExtensionProvider("composing","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider()); + pm.addExtensionProvider("paused","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider()); + pm.addExtensionProvider("inactive","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider()); + pm.addExtensionProvider("gone","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider()); + + // XHTML + pm.addExtensionProvider("html","http://jabber.org/protocol/xhtml-im", new XHTMLExtensionProvider()); + + // Group Chat Invitations + pm.addExtensionProvider("x","jabber:x:conference", new GroupChatInvitation.Provider()); + // Service Discovery # Items + pm.addIQProvider("query","http://jabber.org/protocol/disco#items", new DiscoverItemsProvider()); + // Service Discovery # Info + pm.addIQProvider("query","http://jabber.org/protocol/disco#info", new DiscoverInfoProvider()); + // Data Forms + pm.addExtensionProvider("x","jabber:x:data", new DataFormProvider()); + // MUC User + pm.addExtensionProvider("x","http://jabber.org/protocol/muc#user", new MUCUserProvider()); + // MUC Admin + pm.addIQProvider("query","http://jabber.org/protocol/muc#admin", new MUCAdminProvider()); + // MUC Owner + pm.addIQProvider("query","http://jabber.org/protocol/muc#owner", new MUCOwnerProvider()); + // Delayed Delivery + pm.addExtensionProvider("x","jabber:x:delay", new DelayInformationProvider()); + pm.addExtensionProvider("delay", "urn:xmpp:delay", new DelayInformationProvider()); + // Version + try { + pm.addIQProvider("query","jabber:iq:version", Class.forName("org.jivesoftware.smackx.packet.Version")); + } catch (ClassNotFoundException e) { + System.err.println("Can't load class for org.jivesoftware.smackx.packet.Version"); + } + // VCard + pm.addIQProvider("vCard","vcard-temp", new VCardProvider()); + // Offline Message Requests + pm.addIQProvider("offline","http://jabber.org/protocol/offline", new OfflineMessageRequest.Provider()); + // Offline Message Indicator + pm.addExtensionProvider("offline","http://jabber.org/protocol/offline", new OfflineMessageInfo.Provider()); + // Last Activity + pm.addIQProvider("query","jabber:iq:last", new LastActivity.Provider()); + // User Search + pm.addIQProvider("query","jabber:iq:search", new UserSearch.Provider()); + // SharedGroupsInfo + pm.addIQProvider("sharedgroup","http://www.jivesoftware.org/protocol/sharedgroup", new SharedGroupsInfo.Provider()); + + // JEP-33: Extended Stanza Addressing + pm.addExtensionProvider("addresses","http://jabber.org/protocol/address", new MultipleAddressesProvider()); + + // FileTransfer + pm.addIQProvider("si","http://jabber.org/protocol/si", new StreamInitiationProvider()); + pm.addIQProvider("query","http://jabber.org/protocol/bytestreams", new BytestreamsProvider()); + pm.addIQProvider("open","http://jabber.org/protocol/ibb", new OpenIQProvider()); + pm.addIQProvider("data","http://jabber.org/protocol/ibb", new DataPacketProvider()); + pm.addIQProvider("close","http://jabber.org/protocol/ibb", new CloseIQProvider()); + pm.addExtensionProvider("data","http://jabber.org/protocol/ibb", new DataPacketProvider()); + + // Privacy + pm.addIQProvider("query","jabber:iq:privacy", new PrivacyProvider()); + + // SHIM + pm.addExtensionProvider("headers", "http://jabber.org/protocol/shim", new HeadersProvider()); + pm.addExtensionProvider("header", "http://jabber.org/protocol/shim", new HeaderProvider()); + + // PubSub + pm.addIQProvider("pubsub", "http://jabber.org/protocol/pubsub", new PubSubProvider()); + pm.addExtensionProvider("create", "http://jabber.org/protocol/pubsub", new SimpleNodeProvider()); + pm.addExtensionProvider("items", "http://jabber.org/protocol/pubsub", new ItemsProvider()); + pm.addExtensionProvider("item", "http://jabber.org/protocol/pubsub", new ItemProvider()); + pm.addExtensionProvider("subscriptions", "http://jabber.org/protocol/pubsub", new SubscriptionsProvider()); + pm.addExtensionProvider("subscription", "http://jabber.org/protocol/pubsub", new SubscriptionProvider()); + pm.addExtensionProvider("affiliations", "http://jabber.org/protocol/pubsub", new AffiliationsProvider()); + pm.addExtensionProvider("affiliation", "http://jabber.org/protocol/pubsub", new AffiliationProvider()); + pm.addExtensionProvider("options", "http://jabber.org/protocol/pubsub", new FormNodeProvider()); + // PubSub owner + pm.addIQProvider("pubsub", "http://jabber.org/protocol/pubsub#owner", new PubSubProvider()); + pm.addExtensionProvider("configure", "http://jabber.org/protocol/pubsub#owner", new FormNodeProvider()); + pm.addExtensionProvider("default", "http://jabber.org/protocol/pubsub#owner", new FormNodeProvider()); + // PubSub event + pm.addExtensionProvider("event", "http://jabber.org/protocol/pubsub#event", new EventProvider()); + pm.addExtensionProvider("configuration", "http://jabber.org/protocol/pubsub#event", new ConfigEventProvider()); + pm.addExtensionProvider("delete", "http://jabber.org/protocol/pubsub#event", new SimpleNodeProvider()); + pm.addExtensionProvider("options", "http://jabber.org/protocol/pubsub#event", new FormNodeProvider()); + pm.addExtensionProvider("items", "http://jabber.org/protocol/pubsub#event", new ItemsProvider()); + pm.addExtensionProvider("item", "http://jabber.org/protocol/pubsub#event", new ItemProvider()); + pm.addExtensionProvider("retract", "http://jabber.org/protocol/pubsub#event", new RetractEventProvider()); + pm.addExtensionProvider("purge", "http://jabber.org/protocol/pubsub#event", new SimpleNodeProvider()); + + // Nick Exchange + pm.addExtensionProvider("nick", "http://jabber.org/protocol/nick", new Nick.Provider()); + + // Attention + pm.addExtensionProvider("attention", "urn:xmpp:attention:0", new AttentionExtension.Provider()); + + // XEP-0297 Stanza Forwarding + pm.addExtensionProvider("forwarded", "urn:xmpp:forward:0", new Forwarded.Provider()); + + // XEP-0280 Message Carbons + pm.addExtensionProvider("sent", "urn:xmpp:carbons:2", new Carbon.Provider()); + pm.addExtensionProvider("received", "urn:xmpp:carbons:2", new Carbon.Provider()); + + // XEP-0199 XMPP Ping + pm.addIQProvider("ping", "urn:xmpp:ping", new PingProvider()); + + // XEP-184 Message Delivery Receipts + pm.addExtensionProvider("received", "urn:xmpp:receipts", new DeliveryReceipt.Provider()); + pm.addExtensionProvider("request", "urn:xmpp:receipts", new DeliveryReceipt.Provider()); + + // XEP-0115 Entity Capabilities + pm.addExtensionProvider("c", "http://jabber.org/protocol/caps", new CapsExtensionProvider()); + } +} diff --git a/src/org/jivesoftware/smackx/DefaultMessageEventRequestListener.java b/src/org/jivesoftware/smackx/DefaultMessageEventRequestListener.java new file mode 100644 index 0000000..7ae0c51 --- /dev/null +++ b/src/org/jivesoftware/smackx/DefaultMessageEventRequestListener.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.smackx; + +/** + * + * Default implementation of the MessageEventRequestListener interface.<p> + * + * This class automatically sends a delivered notification to the sender of the message + * if the sender has requested to be notified when the message is delivered. + * + * @author Gaston Dombiak + */ +public class DefaultMessageEventRequestListener implements MessageEventRequestListener { + + public void deliveredNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager) + { + // Send to the message's sender that the message has been delivered + messageEventManager.sendDeliveredNotification(from, packetID); + } + + public void displayedNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager) + { + } + + public void composingNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager) + { + } + + public void offlineNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager) + { + } +} diff --git a/src/org/jivesoftware/smackx/Form.java b/src/org/jivesoftware/smackx/Form.java new file mode 100644 index 0000000..992c036 --- /dev/null +++ b/src/org/jivesoftware/smackx/Form.java @@ -0,0 +1,551 @@ +/** + * $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.smackx; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.StringTokenizer; + +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.packet.DataForm; + +/** + * Represents a Form for gathering data. The form could be of the following types: + * <ul> + * <li>form -> Indicates a form to fill out.</li> + * <li>submit -> The form is filled out, and this is the data that is being returned from + * the form.</li> + * <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li> + * <li>result -> Data results being returned from a search, or some other query.</li> + * </ul> + * + * Depending of the form's type different operations are available. For example, it's only possible + * to set answers if the form is of type "submit". + * + * @see <a href="http://xmpp.org/extensions/xep-0004.html">XEP-0004 Data Forms</a> + * + * @author Gaston Dombiak + */ +public class Form { + + public static final String TYPE_FORM = "form"; + public static final String TYPE_SUBMIT = "submit"; + public static final String TYPE_CANCEL = "cancel"; + public static final String TYPE_RESULT = "result"; + + public static final String NAMESPACE = "jabber:x:data"; + public static final String ELEMENT = "x"; + + private DataForm dataForm; + + /** + * Returns a new ReportedData if the packet is used for gathering data and includes an + * extension that matches the elementName and namespace "x","jabber:x:data". + * + * @param packet the packet used for gathering data. + * @return the data form parsed from the packet or <tt>null</tt> if there was not + * a form in the packet. + */ + public static Form getFormFrom(Packet packet) { + // Check if the packet includes the DataForm extension + PacketExtension packetExtension = packet.getExtension("x","jabber:x:data"); + if (packetExtension != null) { + // Check if the existing DataForm is not a result of a search + DataForm dataForm = (DataForm) packetExtension; + if (dataForm.getReportedData() == null) + return new Form(dataForm); + } + // Otherwise return null + return null; + } + + /** + * Creates a new Form that will wrap an existing DataForm. The wrapped DataForm must be + * used for gathering data. + * + * @param dataForm the data form used for gathering data. + */ + public Form(DataForm dataForm) { + this.dataForm = dataForm; + } + + /** + * Creates a new Form of a given type from scratch.<p> + * + * Possible form types are: + * <ul> + * <li>form -> Indicates a form to fill out.</li> + * <li>submit -> The form is filled out, and this is the data that is being returned from + * the form.</li> + * <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li> + * <li>result -> Data results being returned from a search, or some other query.</li> + * </ul> + * + * @param type the form's type (e.g. form, submit,cancel,result). + */ + public Form(String type) { + this.dataForm = new DataForm(type); + } + + /** + * Adds a new field to complete as part of the form. + * + * @param field the field to complete. + */ + public void addField(FormField field) { + dataForm.addField(field); + } + + /** + * Sets a new String value to a given form's field. The field whose variable matches the + * requested variable will be completed with the specified value. If no field could be found + * for the specified variable then an exception will be raised.<p> + * + * If the value to set to the field is not a basic type (e.g. String, boolean, int, etc.) you + * can use this message where the String value is the String representation of the object. + * + * @param variable the variable name that was completed. + * @param value the String value that was answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable or + * if the answer type does not correspond with the field type.. + */ + public void setAnswer(String variable, String value) { + FormField field = getField(variable); + if (field == null) { + throw new IllegalArgumentException("Field not found for the specified variable name."); + } + if (!FormField.TYPE_TEXT_MULTI.equals(field.getType()) + && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType()) + && !FormField.TYPE_TEXT_SINGLE.equals(field.getType()) + && !FormField.TYPE_JID_SINGLE.equals(field.getType()) + && !FormField.TYPE_HIDDEN.equals(field.getType())) { + throw new IllegalArgumentException("This field is not of type String."); + } + setAnswer(field, value); + } + + /** + * Sets a new int value to a given form's field. The field whose variable matches the + * requested variable will be completed with the specified value. If no field could be found + * for the specified variable then an exception will be raised. + * + * @param variable the variable name that was completed. + * @param value the int value that was answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable or + * if the answer type does not correspond with the field type. + */ + public void setAnswer(String variable, int value) { + FormField field = getField(variable); + if (field == null) { + throw new IllegalArgumentException("Field not found for the specified variable name."); + } + if (!FormField.TYPE_TEXT_MULTI.equals(field.getType()) + && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType()) + && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) { + throw new IllegalArgumentException("This field is not of type int."); + } + setAnswer(field, value); + } + + /** + * Sets a new long value to a given form's field. The field whose variable matches the + * requested variable will be completed with the specified value. If no field could be found + * for the specified variable then an exception will be raised. + * + * @param variable the variable name that was completed. + * @param value the long value that was answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable or + * if the answer type does not correspond with the field type. + */ + public void setAnswer(String variable, long value) { + FormField field = getField(variable); + if (field == null) { + throw new IllegalArgumentException("Field not found for the specified variable name."); + } + if (!FormField.TYPE_TEXT_MULTI.equals(field.getType()) + && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType()) + && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) { + throw new IllegalArgumentException("This field is not of type long."); + } + setAnswer(field, value); + } + + /** + * Sets a new float value to a given form's field. The field whose variable matches the + * requested variable will be completed with the specified value. If no field could be found + * for the specified variable then an exception will be raised. + * + * @param variable the variable name that was completed. + * @param value the float value that was answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable or + * if the answer type does not correspond with the field type. + */ + public void setAnswer(String variable, float value) { + FormField field = getField(variable); + if (field == null) { + throw new IllegalArgumentException("Field not found for the specified variable name."); + } + if (!FormField.TYPE_TEXT_MULTI.equals(field.getType()) + && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType()) + && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) { + throw new IllegalArgumentException("This field is not of type float."); + } + setAnswer(field, value); + } + + /** + * Sets a new double value to a given form's field. The field whose variable matches the + * requested variable will be completed with the specified value. If no field could be found + * for the specified variable then an exception will be raised. + * + * @param variable the variable name that was completed. + * @param value the double value that was answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable or + * if the answer type does not correspond with the field type. + */ + public void setAnswer(String variable, double value) { + FormField field = getField(variable); + if (field == null) { + throw new IllegalArgumentException("Field not found for the specified variable name."); + } + if (!FormField.TYPE_TEXT_MULTI.equals(field.getType()) + && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType()) + && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) { + throw new IllegalArgumentException("This field is not of type double."); + } + setAnswer(field, value); + } + + /** + * Sets a new boolean value to a given form's field. The field whose variable matches the + * requested variable will be completed with the specified value. If no field could be found + * for the specified variable then an exception will be raised. + * + * @param variable the variable name that was completed. + * @param value the boolean value that was answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable or + * if the answer type does not correspond with the field type. + */ + public void setAnswer(String variable, boolean value) { + FormField field = getField(variable); + if (field == null) { + throw new IllegalArgumentException("Field not found for the specified variable name."); + } + if (!FormField.TYPE_BOOLEAN.equals(field.getType())) { + throw new IllegalArgumentException("This field is not of type boolean."); + } + setAnswer(field, (value ? "1" : "0")); + } + + /** + * Sets a new Object value to a given form's field. In fact, the object representation + * (i.e. #toString) will be the actual value of the field.<p> + * + * If the value to set to the field is not a basic type (e.g. String, boolean, int, etc.) you + * will need to use {@link #setAnswer(String, String))} where the String value is the + * String representation of the object.<p> + * + * Before setting the new value to the field we will check if the form is of type submit. If + * the form isn't of type submit means that it's not possible to complete the form and an + * exception will be thrown. + * + * @param field the form field that was completed. + * @param value the Object value that was answered. The object representation will be the + * actual value. + * @throws IllegalStateException if the form is not of type "submit". + */ + private void setAnswer(FormField field, Object value) { + if (!isSubmitType()) { + throw new IllegalStateException("Cannot set an answer if the form is not of type " + + "\"submit\""); + } + field.resetValues(); + field.addValue(value.toString()); + } + + /** + * Sets a new values to a given form's field. The field whose variable matches the requested + * variable will be completed with the specified values. If no field could be found for + * the specified variable then an exception will be raised.<p> + * + * The Objects contained in the List could be of any type. The String representation of them + * (i.e. #toString) will be actually used when sending the answer to the server. + * + * @param variable the variable that was completed. + * @param values the values that were answered. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable. + */ + public void setAnswer(String variable, List<String> values) { + if (!isSubmitType()) { + throw new IllegalStateException("Cannot set an answer if the form is not of type " + + "\"submit\""); + } + FormField field = getField(variable); + if (field != null) { + // Check that the field can accept a collection of values + if (!FormField.TYPE_JID_MULTI.equals(field.getType()) + && !FormField.TYPE_LIST_MULTI.equals(field.getType()) + && !FormField.TYPE_LIST_SINGLE.equals(field.getType()) + && !FormField.TYPE_TEXT_MULTI.equals(field.getType()) + && !FormField.TYPE_HIDDEN.equals(field.getType())) { + throw new IllegalArgumentException("This field only accept list of values."); + } + // Clear the old values + field.resetValues(); + // Set the new values. The string representation of each value will be actually used. + field.addValues(values); + } + else { + throw new IllegalArgumentException("Couldn't find a field for the specified variable."); + } + } + + /** + * Sets the default value as the value of a given form's field. The field whose variable matches + * the requested variable will be completed with its default value. If no field could be found + * for the specified variable then an exception will be raised. + * + * @param variable the variable to complete with its default value. + * @throws IllegalStateException if the form is not of type "submit". + * @throws IllegalArgumentException if the form does not include the specified variable. + */ + public void setDefaultAnswer(String variable) { + if (!isSubmitType()) { + throw new IllegalStateException("Cannot set an answer if the form is not of type " + + "\"submit\""); + } + FormField field = getField(variable); + if (field != null) { + // Clear the old values + field.resetValues(); + // Set the default value + for (Iterator<String> it = field.getValues(); it.hasNext();) { + field.addValue(it.next()); + } + } + else { + throw new IllegalArgumentException("Couldn't find a field for the specified variable."); + } + } + + /** + * Returns an Iterator for the fields that are part of the form. + * + * @return an Iterator for the fields that are part of the form. + */ + public Iterator<FormField> getFields() { + return dataForm.getFields(); + } + + /** + * Returns the field of the form whose variable matches the specified variable. + * The fields of type FIXED will never be returned since they do not specify a + * variable. + * + * @param variable the variable to look for in the form fields. + * @return the field of the form whose variable matches the specified variable. + */ + public FormField getField(String variable) { + if (variable == null || variable.equals("")) { + throw new IllegalArgumentException("Variable must not be null or blank."); + } + // Look for the field whose variable matches the requested variable + FormField field; + for (Iterator<FormField> it=getFields();it.hasNext();) { + field = it.next(); + if (variable.equals(field.getVariable())) { + return field; + } + } + return null; + } + + /** + * Returns the instructions that explain how to fill out the form and what the form is about. + * + * @return instructions that explain how to fill out the form. + */ + public String getInstructions() { + StringBuilder sb = new StringBuilder(); + // Join the list of instructions together separated by newlines + for (Iterator<String> it = dataForm.getInstructions(); it.hasNext();) { + sb.append(it.next()); + // If this is not the last instruction then append a newline + if (it.hasNext()) { + sb.append("\n"); + } + } + return sb.toString(); + } + + + /** + * Returns the description of the data. It is similar to the title on a web page or an X + * window. You can put a <title/> on either a form to fill out, or a set of data results. + * + * @return description of the data. + */ + public String getTitle() { + return dataForm.getTitle(); + } + + + /** + * Returns the meaning of the data within the context. The data could be part of a form + * to fill out, a form submission or data results.<p> + * + * Possible form types are: + * <ul> + * <li>form -> Indicates a form to fill out.</li> + * <li>submit -> The form is filled out, and this is the data that is being returned from + * the form.</li> + * <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li> + * <li>result -> Data results being returned from a search, or some other query.</li> + * </ul> + * + * @return the form's type. + */ + public String getType() { + return dataForm.getType(); + } + + + /** + * Sets instructions that explain how to fill out the form and what the form is about. + * + * @param instructions instructions that explain how to fill out the form. + */ + public void setInstructions(String instructions) { + // Split the instructions into multiple instructions for each existent newline + ArrayList<String> instructionsList = new ArrayList<String>(); + StringTokenizer st = new StringTokenizer(instructions, "\n"); + while (st.hasMoreTokens()) { + instructionsList.add(st.nextToken()); + } + // Set the new list of instructions + dataForm.setInstructions(instructionsList); + + } + + + /** + * Sets the description of the data. It is similar to the title on a web page or an X window. + * You can put a <title/> on either a form to fill out, or a set of data results. + * + * @param title description of the data. + */ + public void setTitle(String title) { + dataForm.setTitle(title); + } + + /** + * Returns a DataForm that serves to send this Form to the server. If the form is of type + * submit, it may contain fields with no value. These fields will be removed since they only + * exist to assist the user while editing/completing the form in a UI. + * + * @return the wrapped DataForm. + */ + public DataForm getDataFormToSend() { + if (isSubmitType()) { + // Create a new DataForm that contains only the answered fields + DataForm dataFormToSend = new DataForm(getType()); + for(Iterator<FormField> it=getFields();it.hasNext();) { + FormField field = it.next(); + if (field.getValues().hasNext()) { + dataFormToSend.addField(field); + } + } + return dataFormToSend; + } + return dataForm; + } + + /** + * Returns true if the form is a form to fill out. + * + * @return if the form is a form to fill out. + */ + private boolean isFormType() { + return TYPE_FORM.equals(dataForm.getType()); + } + + /** + * Returns true if the form is a form to submit. + * + * @return if the form is a form to submit. + */ + private boolean isSubmitType() { + return TYPE_SUBMIT.equals(dataForm.getType()); + } + + /** + * Returns a new Form to submit the completed values. The new Form will include all the fields + * of the original form except for the fields of type FIXED. Only the HIDDEN fields will + * include the same value of the original form. The other fields of the new form MUST be + * completed. If a field remains with no answer when sending the completed form, then it won't + * be included as part of the completed form.<p> + * + * The reason why the fields with variables are included in the new form is to provide a model + * for binding with any UI. This means that the UIs will use the original form (of type + * "form") to learn how to render the form, but the UIs will bind the fields to the form of + * type submit. + * + * @return a Form to submit the completed values. + */ + public Form createAnswerForm() { + if (!isFormType()) { + throw new IllegalStateException("Only forms of type \"form\" could be answered"); + } + // Create a new Form + Form form = new Form(TYPE_SUBMIT); + for (Iterator<FormField> fields=getFields(); fields.hasNext();) { + FormField field = fields.next(); + // Add to the new form any type of field that includes a variable. + // Note: The fields of type FIXED are the only ones that don't specify a variable + if (field.getVariable() != null) { + FormField newField = new FormField(field.getVariable()); + newField.setType(field.getType()); + form.addField(newField); + // Set the answer ONLY to the hidden fields + if (FormField.TYPE_HIDDEN.equals(field.getType())) { + // Since a hidden field could have many values we need to collect them + // in a list + List<String> values = new ArrayList<String>(); + for (Iterator<String> it=field.getValues();it.hasNext();) { + values.add(it.next()); + } + form.setAnswer(field.getVariable(), values); + } + } + } + return form; + } + +} diff --git a/src/org/jivesoftware/smackx/FormField.java b/src/org/jivesoftware/smackx/FormField.java new file mode 100644 index 0000000..3d15e92 --- /dev/null +++ b/src/org/jivesoftware/smackx/FormField.java @@ -0,0 +1,409 @@ +/** + * $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.smackx; + +import org.jivesoftware.smack.util.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Represents a field of a form. The field could be used to represent a question to complete, + * a completed question or a data returned from a search. The exact interpretation of the field + * depends on the context where the field is used. + * + * @author Gaston Dombiak + */ +public class FormField { + + public static final String TYPE_BOOLEAN = "boolean"; + public static final String TYPE_FIXED = "fixed"; + public static final String TYPE_HIDDEN = "hidden"; + public static final String TYPE_JID_MULTI = "jid-multi"; + public static final String TYPE_JID_SINGLE = "jid-single"; + public static final String TYPE_LIST_MULTI = "list-multi"; + public static final String TYPE_LIST_SINGLE = "list-single"; + public static final String TYPE_TEXT_MULTI = "text-multi"; + public static final String TYPE_TEXT_PRIVATE = "text-private"; + public static final String TYPE_TEXT_SINGLE = "text-single"; + + private String description; + private boolean required = false; + private String label; + private String variable; + private String type; + private final List<Option> options = new ArrayList<Option>(); + private final List<String> values = new ArrayList<String>(); + + /** + * Creates a new FormField with the variable name that uniquely identifies the field + * in the context of the form. + * + * @param variable the variable name of the question. + */ + public FormField(String variable) { + this.variable = variable; + } + + /** + * Creates a new FormField of type FIXED. The fields of type FIXED do not define a variable + * name. + */ + public FormField() { + this.type = FormField.TYPE_FIXED; + } + + /** + * Returns a description that provides extra clarification about the question. This information + * could be presented to the user either in tool-tip, help button, or as a section of text + * before the question.<p> + * <p/> + * If the question is of type FIXED then the description should remain empty. + * + * @return description that provides extra clarification about the question. + */ + public String getDescription() { + return description; + } + + /** + * Returns the label of the question which should give enough information to the user to + * fill out the form. + * + * @return label of the question. + */ + public String getLabel() { + return label; + } + + /** + * Returns an Iterator for the available options that the user has in order to answer + * the question. + * + * @return Iterator for the available options. + */ + public Iterator<Option> getOptions() { + synchronized (options) { + return Collections.unmodifiableList(new ArrayList<Option>(options)).iterator(); + } + } + + /** + * Returns true if the question must be answered in order to complete the questionnaire. + * + * @return true if the question must be answered in order to complete the questionnaire. + */ + public boolean isRequired() { + return required; + } + + /** + * Returns an indicative of the format for the data to answer. Valid formats are: + * <p/> + * <ul> + * <li>text-single -> single line or word of text + * <li>text-private -> instead of showing the user what they typed, you show ***** to + * protect it + * <li>text-multi -> multiple lines of text entry + * <li>list-single -> given a list of choices, pick one + * <li>list-multi -> given a list of choices, pick one or more + * <li>boolean -> 0 or 1, true or false, yes or no. Default value is 0 + * <li>fixed -> fixed for putting in text to show sections, or just advertise your web + * site in the middle of the form + * <li>hidden -> is not given to the user at all, but returned with the questionnaire + * <li>jid-single -> Jabber ID - choosing a JID from your roster, and entering one based + * on the rules for a JID. + * <li>jid-multi -> multiple entries for JIDs + * </ul> + * + * @return format for the data to answer. + */ + public String getType() { + return type; + } + + /** + * Returns an Iterator for the default values of the question if the question is part + * of a form to fill out. Otherwise, returns an Iterator for the answered values of + * the question. + * + * @return an Iterator for the default values or answered values of the question. + */ + public Iterator<String> getValues() { + synchronized (values) { + return Collections.unmodifiableList(new ArrayList<String>(values)).iterator(); + } + } + + /** + * Returns the variable name that the question is filling out. + * + * @return the variable name of the question. + */ + public String getVariable() { + return variable; + } + + /** + * Sets a description that provides extra clarification about the question. This information + * could be presented to the user either in tool-tip, help button, or as a section of text + * before the question.<p> + * <p/> + * If the question is of type FIXED then the description should remain empty. + * + * @param description provides extra clarification about the question. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Sets the label of the question which should give enough information to the user to + * fill out the form. + * + * @param label the label of the question. + */ + public void setLabel(String label) { + this.label = label; + } + + /** + * Sets if the question must be answered in order to complete the questionnaire. + * + * @param required if the question must be answered in order to complete the questionnaire. + */ + public void setRequired(boolean required) { + this.required = required; + } + + /** + * Sets an indicative of the format for the data to answer. Valid formats are: + * <p/> + * <ul> + * <li>text-single -> single line or word of text + * <li>text-private -> instead of showing the user what they typed, you show ***** to + * protect it + * <li>text-multi -> multiple lines of text entry + * <li>list-single -> given a list of choices, pick one + * <li>list-multi -> given a list of choices, pick one or more + * <li>boolean -> 0 or 1, true or false, yes or no. Default value is 0 + * <li>fixed -> fixed for putting in text to show sections, or just advertise your web + * site in the middle of the form + * <li>hidden -> is not given to the user at all, but returned with the questionnaire + * <li>jid-single -> Jabber ID - choosing a JID from your roster, and entering one based + * on the rules for a JID. + * <li>jid-multi -> multiple entries for JIDs + * </ul> + * + * @param type an indicative of the format for the data to answer. + */ + public void setType(String type) { + this.type = type; + } + + /** + * Adds a default value to the question if the question is part of a form to fill out. + * Otherwise, adds an answered value to the question. + * + * @param value a default value or an answered value of the question. + */ + public void addValue(String value) { + synchronized (values) { + values.add(value); + } + } + + /** + * Adds a default values to the question if the question is part of a form to fill out. + * Otherwise, adds an answered values to the question. + * + * @param newValues default values or an answered values of the question. + */ + public void addValues(List<String> newValues) { + synchronized (values) { + values.addAll(newValues); + } + } + + /** + * Removes all the values of the field. + */ + protected void resetValues() { + synchronized (values) { + values.removeAll(new ArrayList<String>(values)); + } + } + + /** + * Adss an available options to the question that the user has in order to answer + * the question. + * + * @param option a new available option for the question. + */ + public void addOption(Option option) { + synchronized (options) { + options.add(option); + } + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<field"); + // Add attributes + if (getLabel() != null) { + buf.append(" label=\"").append(getLabel()).append("\""); + } + if (getVariable() != null) { + buf.append(" var=\"").append(getVariable()).append("\""); + } + if (getType() != null) { + buf.append(" type=\"").append(getType()).append("\""); + } + buf.append(">"); + // Add elements + if (getDescription() != null) { + buf.append("<desc>").append(getDescription()).append("</desc>"); + } + if (isRequired()) { + buf.append("<required/>"); + } + // Loop through all the values and append them to the string buffer + for (Iterator<String> i = getValues(); i.hasNext();) { + buf.append("<value>").append(i.next()).append("</value>"); + } + // Loop through all the values and append them to the string buffer + for (Iterator<Option> i = getOptions(); i.hasNext();) { + buf.append((i.next()).toXML()); + } + buf.append("</field>"); + return buf.toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) + return false; + if (obj == this) + return true; + if (!(obj instanceof FormField)) + return false; + + FormField other = (FormField) obj; + + return toXML().equals(other.toXML()); + } + + @Override + public int hashCode() { + return toXML().hashCode(); + } + + /** + * Represents the available option of a given FormField. + * + * @author Gaston Dombiak + */ + public static class Option { + + private String label; + private String value; + + public Option(String value) { + this.value = value; + } + + public Option(String label, String value) { + this.label = label; + this.value = value; + } + + /** + * Returns the label that represents the option. + * + * @return the label that represents the option. + */ + public String getLabel() { + return label; + } + + /** + * Returns the value of the option. + * + * @return the value of the option. + */ + public String getValue() { + return value; + } + + @Override + public String toString() { + return getLabel(); + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<option"); + // Add attribute + if (getLabel() != null) { + buf.append(" label=\"").append(getLabel()).append("\""); + } + buf.append(">"); + // Add element + buf.append("<value>").append(StringUtils.escapeForXML(getValue())).append("</value>"); + + buf.append("</option>"); + return buf.toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) + return false; + if (obj == this) + return true; + if (obj.getClass() != getClass()) + return false; + + Option other = (Option) obj; + + if (!value.equals(other.value)) + return false; + + String thisLabel = label == null ? "" : label; + String otherLabel = other.label == null ? "" : other.label; + + if (!thisLabel.equals(otherLabel)) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = 1; + result = 37 * result + value.hashCode(); + result = 37 * result + (label == null ? 0 : label.hashCode()); + return result; + } + } +} diff --git a/src/org/jivesoftware/smackx/Gateway.java b/src/org/jivesoftware/smackx/Gateway.java new file mode 100644 index 0000000..5b5836f --- /dev/null +++ b/src/org/jivesoftware/smackx/Gateway.java @@ -0,0 +1,333 @@ +package org.jivesoftware.smackx; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.Roster; +import org.jivesoftware.smack.RosterEntry; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPException; +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.Registration; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverInfo.Identity; + +/** + * This class provides an abstract view to gateways/transports. This class handles all + * actions regarding gateways and transports. + * @author Till Klocke + * + */ +public class Gateway { + + private Connection connection; + private ServiceDiscoveryManager sdManager; + private Roster roster; + private String entityJID; + private Registration registerInfo; + private Identity identity; + private DiscoverInfo info; + + Gateway(Connection connection, String entityJID){ + this.connection = connection; + this.roster = connection.getRoster(); + this.sdManager = ServiceDiscoveryManager.getInstanceFor(connection); + this.entityJID = entityJID; + } + + Gateway(Connection connection, String entityJID, DiscoverInfo info, Identity identity){ + this(connection, entityJID); + this.info = info; + this.identity = identity; + } + + private void discoverInfo() throws XMPPException{ + info = sdManager.discoverInfo(entityJID); + Iterator<Identity> iterator = info.getIdentities(); + while(iterator.hasNext()){ + Identity temp = iterator.next(); + if(temp.getCategory().equalsIgnoreCase("gateway")){ + this.identity = temp; + break; + } + } + } + + private Identity getIdentity() throws XMPPException{ + if(identity==null){ + discoverInfo(); + } + return identity; + } + + private Registration getRegisterInfo(){ + if(registerInfo==null){ + refreshRegisterInfo(); + } + return registerInfo; + } + + private void refreshRegisterInfo(){ + Registration packet = new Registration(); + packet.setFrom(connection.getUser()); + packet.setType(IQ.Type.GET); + packet.setTo(entityJID); + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(packet.getPacketID())); + connection.sendPacket(packet); + Packet result = collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if(result instanceof Registration && result.getError()==null){ + Registration register = (Registration)result; + this.registerInfo = register; + } + } + + /** + * Checks if this gateway supports In-Band registration + * @return true if In-Band registration is supported + * @throws XMPPException + */ + public boolean canRegister() throws XMPPException{ + if(info==null){ + discoverInfo(); + } + return info.containsFeature("jabber:iq:register"); + } + + /** + * Returns all fields that are required to register to this gateway + * @return a list of required fields + */ + public List<String> getRequiredFields(){ + return getRegisterInfo().getRequiredFields(); + } + + /** + * Returns the name as proposed in this gateways identity discovered via service + * discovery + * @return a String of its name + * @throws XMPPException + */ + public String getName() throws XMPPException{ + if(identity==null){ + discoverInfo(); + } + return identity.getName(); + } + + /** + * Returns the type as proposed in this gateways identity discovered via service + * discovery. See {@link http://xmpp.org/registrar/disco-categories.html} for + * possible types + * @return a String describing the type + * @throws XMPPException + */ + public String getType() throws XMPPException{ + if(identity==null){ + discoverInfo(); + } + return identity.getType(); + } + + /** + * Returns true if the registration informations indicates that you are already + * registered with this gateway + * @return true if already registered + * @throws XMPPException + */ + public boolean isRegistered() throws XMPPException{ + return getRegisterInfo().isRegistered(); + } + + /** + * Returns the value of specific field of the registration information. Can be used + * to retrieve for example to retrieve username/password used on an already registered + * gateway. + * @param fieldName name of the field + * @return a String containing the value of the field or null + */ + public String getField(String fieldName){ + return getRegisterInfo().getField(fieldName); + } + + /** + * Returns a List of Strings of all field names which contain values. + * @return a List of field names + */ + public List<String> getFieldNames(){ + return getRegisterInfo().getFieldNames(); + } + + /** + * A convenience method for retrieving the username of an existing account + * @return String describing the username + */ + public String getUsername(){ + return getField("username"); + } + + /** + * A convenience method for retrieving the password of an existing accoung + * @return String describing the password + */ + public String getPassword(){ + return getField("password"); + } + + /** + * Returns instructions for registering with this gateway + * @return String containing instructions + */ + public String getInstructions(){ + return getRegisterInfo().getInstructions(); + } + + /** + * With this method you can register with this gateway or modify an existing registration + * @param username String describing the username + * @param password String describing the password + * @param fields additional fields like email. + * @throws XMPPException + */ + public void register(String username, String password, Map<String,String> fields)throws XMPPException{ + if(getRegisterInfo().isRegistered()) { + throw new IllegalStateException("You are already registered with this gateway"); + } + Registration register = new Registration(); + register.setFrom(connection.getUser()); + register.setTo(entityJID); + register.setType(IQ.Type.SET); + register.setUsername(username); + register.setPassword(password); + for(String s : fields.keySet()){ + register.addAttribute(s, fields.get(s)); + } + PacketCollector resultCollector = + connection.createPacketCollector(new PacketIDFilter(register.getPacketID())); + connection.sendPacket(register); + Packet result = + resultCollector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + resultCollector.cancel(); + if(result!=null && result instanceof IQ){ + IQ resultIQ = (IQ)result; + if(resultIQ.getError()!=null){ + throw new XMPPException(resultIQ.getError()); + } + if(resultIQ.getType()==IQ.Type.ERROR){ + throw new XMPPException(resultIQ.getError()); + } + connection.addPacketListener(new GatewayPresenceListener(), + new PacketTypeFilter(Presence.class)); + roster.createEntry(entityJID, getIdentity().getName(), new String[]{}); + } + else{ + throw new XMPPException("Packet reply timeout"); + } + } + + /** + * A convenience method for registering or modifying an account on this gateway without + * additional fields + * @param username String describing the username + * @param password String describing the password + * @throws XMPPException + */ + public void register(String username, String password) throws XMPPException{ + register(username, password,new HashMap<String,String>()); + } + + /** + * This method removes an existing registration from this gateway + * @throws XMPPException + */ + public void unregister() throws XMPPException{ + Registration register = new Registration(); + register.setFrom(connection.getUser()); + register.setTo(entityJID); + register.setType(IQ.Type.SET); + register.setRemove(true); + PacketCollector resultCollector = + connection.createPacketCollector(new PacketIDFilter(register.getPacketID())); + connection.sendPacket(register); + Packet result = resultCollector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + resultCollector.cancel(); + if(result!=null && result instanceof IQ){ + IQ resultIQ = (IQ)result; + if(resultIQ.getError()!=null){ + throw new XMPPException(resultIQ.getError()); + } + if(resultIQ.getType()==IQ.Type.ERROR){ + throw new XMPPException(resultIQ.getError()); + } + RosterEntry gatewayEntry = roster.getEntry(entityJID); + roster.removeEntry(gatewayEntry); + } + else{ + throw new XMPPException("Packet reply timeout"); + } + } + + /** + * Lets you login manually in this gateway. Normally a gateway logins you when it + * receives the first presence broadcasted by your server. But it is possible to + * manually login and logout by sending a directed presence. This method sends an + * empty available presence direct to the gateway. + */ + public void login(){ + Presence presence = new Presence(Presence.Type.available); + login(presence); + } + + /** + * This method lets you send the presence direct to the gateway. Type, To and From + * are modified. + * @param presence the presence used to login to gateway + */ + public void login(Presence presence){ + presence.setType(Presence.Type.available); + presence.setTo(entityJID); + presence.setFrom(connection.getUser()); + connection.sendPacket(presence); + } + + /** + * This method logs you out from this gateway by sending an unavailable presence + * to directly to this gateway. + */ + public void logout(){ + Presence presence = new Presence(Presence.Type.unavailable); + presence.setTo(entityJID); + presence.setFrom(connection.getUser()); + connection.sendPacket(presence); + } + + private class GatewayPresenceListener implements PacketListener{ + + public void processPacket(Packet packet) { + if(packet instanceof Presence){ + Presence presence = (Presence)packet; + if(entityJID.equals(presence.getFrom()) && + roster.contains(presence.getFrom()) && + presence.getType().equals(Presence.Type.subscribe)){ + Presence response = new Presence(Presence.Type.subscribed); + response.setTo(presence.getFrom()); + response.setFrom(StringUtils.parseBareAddress(connection.getUser())); + connection.sendPacket(response); + } + } + + } + } + +} diff --git a/src/org/jivesoftware/smackx/GatewayManager.java b/src/org/jivesoftware/smackx/GatewayManager.java new file mode 100644 index 0000000..eee9cfc --- /dev/null +++ b/src/org/jivesoftware/smackx/GatewayManager.java @@ -0,0 +1,199 @@ +package org.jivesoftware.smackx; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.Roster; +import org.jivesoftware.smack.RosterEntry; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverItems; +import org.jivesoftware.smackx.packet.DiscoverInfo.Identity; +import org.jivesoftware.smackx.packet.DiscoverItems.Item; + +/** + * This class is the general entry point to gateway interaction (XEP-0100). + * This class discovers available gateways on the users servers, and + * can give you also a list of gateways the you user is registered with which + * are not on his server. All actual interaction with a gateway is handled in the + * class {@see Gateway}. + * @author Till Klocke + * + */ +public class GatewayManager { + + private static Map<Connection,GatewayManager> instances = + new HashMap<Connection,GatewayManager>(); + + private ServiceDiscoveryManager sdManager; + + private Map<String,Gateway> localGateways = new HashMap<String,Gateway>(); + + private Map<String,Gateway> nonLocalGateways = new HashMap<String,Gateway>(); + + private Map<String,Gateway> gateways = new HashMap<String,Gateway>(); + + private Connection connection; + + private Roster roster; + + private GatewayManager(){ + + } + + /** + * Creates a new instance of GatewayManager + * @param connection + * @throws XMPPException + */ + private GatewayManager(Connection connection) throws XMPPException{ + this.connection = connection; + this.roster = connection.getRoster(); + sdManager = ServiceDiscoveryManager.getInstanceFor(connection); + } + + /** + * Loads all gateways the users server offers + * @throws XMPPException + */ + private void loadLocalGateways() throws XMPPException{ + DiscoverItems items = sdManager.discoverItems(connection.getHost()); + Iterator<Item> iter = items.getItems(); + while(iter.hasNext()){ + String itemJID = iter.next().getEntityID(); + discoverGateway(itemJID); + } + } + + /** + * Discovers {@link DiscoveryInfo} and {@link DiscoveryInfo.Identity} of a gateway + * and creates a {@link Gateway} object representing this gateway. + * @param itemJID + * @throws XMPPException + */ + private void discoverGateway(String itemJID) throws XMPPException{ + DiscoverInfo info = sdManager.discoverInfo(itemJID); + Iterator<Identity> i = info.getIdentities(); + + while(i.hasNext()){ + Identity identity = i.next(); + String category = identity.getCategory(); + if(category.toLowerCase().equals("gateway")){ + gateways.put(itemJID, new Gateway(connection,itemJID)); + if(itemJID.contains(connection.getHost())){ + localGateways.put(itemJID, + new Gateway(connection,itemJID,info,identity)); + } + else{ + nonLocalGateways.put(itemJID, + new Gateway(connection,itemJID,info,identity)); + } + break; + } + } + } + + /** + * Loads all getways which are in the users roster, but are not supplied by the + * users server + * @throws XMPPException + */ + private void loadNonLocalGateways() throws XMPPException{ + if(roster!=null){ + for(RosterEntry entry : roster.getEntries()){ + if(entry.getUser().equalsIgnoreCase(StringUtils.parseServer(entry.getUser())) && + !entry.getUser().contains(connection.getHost())){ + discoverGateway(entry.getUser()); + } + } + } + } + + /** + * Returns an instance of GatewayManager for the given connection. If no instance for + * this connection exists a new one is created and stored in a Map. + * @param connection + * @return an instance of GatewayManager + * @throws XMPPException + */ + public GatewayManager getInstanceFor(Connection connection) throws XMPPException{ + synchronized(instances){ + if(instances.containsKey(connection)){ + return instances.get(connection); + } + GatewayManager instance = new GatewayManager(connection); + instances.put(connection, instance); + return instance; + } + } + + /** + * Returns a list of gateways which are offered by the users server, wether the + * user is registered to them or not. + * @return a List of Gateways + * @throws XMPPException + */ + public List<Gateway> getLocalGateways() throws XMPPException{ + if(localGateways.size()==0){ + loadLocalGateways(); + } + return new ArrayList<Gateway>(localGateways.values()); + } + + /** + * Returns a list of gateways the user has in his roster, but which are offered by + * remote servers. But note that this list isn't automatically refreshed. You have to + * refresh is manually if needed. + * @return a list of gateways + * @throws XMPPException + */ + public List<Gateway> getNonLocalGateways() throws XMPPException{ + if(nonLocalGateways.size()==0){ + loadNonLocalGateways(); + } + return new ArrayList<Gateway>(nonLocalGateways.values()); + } + + /** + * Refreshes the list of gateways offered by remote servers. + * @throws XMPPException + */ + public void refreshNonLocalGateways() throws XMPPException{ + loadNonLocalGateways(); + } + + /** + * Returns a Gateway object for a given JID. Please note that it is not checked if + * the JID belongs to valid gateway. If this JID doesn't belong to valid gateway + * all operations on this Gateway object should fail with a XMPPException. But there is + * no guarantee for that. + * @param entityJID + * @return a Gateway object + */ + public Gateway getGateway(String entityJID){ + if(localGateways.containsKey(entityJID)){ + return localGateways.get(entityJID); + } + if(nonLocalGateways.containsKey(entityJID)){ + return nonLocalGateways.get(entityJID); + } + if(gateways.containsKey(entityJID)){ + return gateways.get(entityJID); + } + Gateway gateway = new Gateway(connection,entityJID); + if(entityJID.contains(connection.getHost())){ + localGateways.put(entityJID, gateway); + } + else{ + nonLocalGateways.put(entityJID, gateway); + } + gateways.put(entityJID, gateway); + return gateway; + } + +} diff --git a/src/org/jivesoftware/smackx/GroupChatInvitation.java b/src/org/jivesoftware/smackx/GroupChatInvitation.java new file mode 100644 index 0000000..a9ed35e --- /dev/null +++ b/src/org/jivesoftware/smackx/GroupChatInvitation.java @@ -0,0 +1,115 @@ +/** + * $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.smackx; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.xmlpull.v1.XmlPullParser; + +/** + * A group chat invitation packet extension, which is used to invite other + * users to a group chat room. To invite a user to a group chat room, address + * a new message to the user and set the room name appropriately, as in the + * following code example: + * + * <pre> + * Message message = new Message("user@chat.example.com"); + * message.setBody("Join me for a group chat!"); + * message.addExtension(new GroupChatInvitation("room@chat.example.com");); + * con.sendPacket(message); + * </pre> + * + * To listen for group chat invitations, use a PacketExtensionFilter for the + * <tt>x</tt> element name and <tt>jabber:x:conference</tt> namespace, as in the + * following code example: + * + * <pre> + * PacketFilter filter = new PacketExtensionFilter("x", "jabber:x:conference"); + * // Create a packet collector or packet listeners using the filter... + * </pre> + * + * <b>Note</b>: this protocol is outdated now that the Multi-User Chat (MUC) JEP is available + * (<a href="http://www.jabber.org/jeps/jep-0045.html">JEP-45</a>). However, most + * existing clients still use this older protocol. Once MUC support becomes more + * widespread, this API may be deprecated. + * + * @author Matt Tucker + */ +public class GroupChatInvitation implements PacketExtension { + + /** + * Element name of the packet extension. + */ + public static final String ELEMENT_NAME = "x"; + + /** + * Namespace of the packet extension. + */ + public static final String NAMESPACE = "jabber:x:conference"; + + private String roomAddress; + + /** + * Creates a new group chat invitation to the specified room address. + * GroupChat room addresses are in the form <tt>room@service</tt>, + * where <tt>service</tt> is the name of groupchat server, such as + * <tt>chat.example.com</tt>. + * + * @param roomAddress the address of the group chat room. + */ + public GroupChatInvitation(String roomAddress) { + this.roomAddress = roomAddress; + } + + /** + * Returns the address of the group chat room. GroupChat room addresses + * are in the form <tt>room@service</tt>, where <tt>service</tt> is + * the name of groupchat server, such as <tt>chat.example.com</tt>. + * + * @return the address of the group chat room. + */ + public String getRoomAddress() { + return roomAddress; + } + + public String getElementName() { + return ELEMENT_NAME; + } + + public String getNamespace() { + return NAMESPACE; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<x xmlns=\"jabber:x:conference\" jid=\"").append(roomAddress).append("\"/>"); + return buf.toString(); + } + + public static class Provider implements PacketExtensionProvider { + public PacketExtension parseExtension (XmlPullParser parser) throws Exception { + String roomAddress = parser.getAttributeValue("", "jid"); + // Advance to end of extension. + parser.next(); + return new GroupChatInvitation(roomAddress); + } + } +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/InitStaticCode.java b/src/org/jivesoftware/smackx/InitStaticCode.java new file mode 100644 index 0000000..12de5af --- /dev/null +++ b/src/org/jivesoftware/smackx/InitStaticCode.java @@ -0,0 +1,51 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx; + +import android.content.Context; + +/** + * Since dalvik on Android does not allow the loading of META-INF files from the + * filesystem, the static blocks of some classes have to be inited manually. + * + * The full list can be found here: + * http://fisheye.igniterealtime.org/browse/smack/trunk/build/resources/META-INF/smack-config.xml?hb=true + * + * @author Florian Schmaus fschmaus@gmail.com + * + */ +public class InitStaticCode { + + public static void initStaticCode(Context ctx) { + // This has the be the application class loader, + // *not* the system class loader + ClassLoader appClassLoader = ctx.getClassLoader(); + + try { + Class.forName(org.jivesoftware.smackx.ServiceDiscoveryManager.class.getName(), true, appClassLoader); + Class.forName(org.jivesoftware.smack.PrivacyListManager.class.getName(), true, appClassLoader); + Class.forName(org.jivesoftware.smackx.XHTMLManager.class.getName(), true, appClassLoader); + Class.forName(org.jivesoftware.smackx.muc.MultiUserChat.class.getName(), true, appClassLoader); + Class.forName(org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.class.getName(), true, appClassLoader); + Class.forName(org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager.class.getName(), true, appClassLoader); + Class.forName(org.jivesoftware.smackx.filetransfer.FileTransferManager.class.getName(), true, appClassLoader); + Class.forName(org.jivesoftware.smackx.LastActivityManager.class.getName(), true, appClassLoader); + Class.forName(org.jivesoftware.smack.ReconnectionManager.class.getName(), true, appClassLoader); + Class.forName(org.jivesoftware.smackx.commands.AdHocCommandManager.class.getName(), true, appClassLoader); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Could not init static class blocks", e); + } + } +} diff --git a/src/org/jivesoftware/smackx/LastActivityManager.java b/src/org/jivesoftware/smackx/LastActivityManager.java new file mode 100644 index 0000000..a9d1f12 --- /dev/null +++ b/src/org/jivesoftware/smackx/LastActivityManager.java @@ -0,0 +1,230 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jivesoftware.smackx;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.IQTypeFilter;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.LastActivity;
+
+/**
+ * A last activity manager for handling information about the last activity
+ * associated with a Jabber ID. A manager handles incoming LastActivity requests
+ * of existing Connections. It also allows to request last activity information
+ * of other users.
+ * <p>
+ *
+ * LastActivity (XEP-0012) based on the sending JID's type allows for retrieval
+ * of:
+ * <ol>
+ * <li>How long a particular user has been idle
+ * <li>How long a particular user has been logged-out and the message the
+ * specified when doing so.
+ * <li>How long a host has been up.
+ * </ol>
+ * <p/>
+ *
+ * For example to get the idle time of a user logged in a resource, simple send
+ * the LastActivity packet to them, as in the following code:
+ * <p>
+ *
+ * <pre>
+ * Connection con = new XMPPConnection("jabber.org");
+ * con.login("john", "doe");
+ * LastActivity activity = LastActivity.getLastActivity(con, "xray@jabber.org/Smack");
+ * </pre>
+ *
+ * To get the lapsed time since the last user logout is the same as above but
+ * with out the resource:
+ *
+ * <pre>
+ * LastActivity activity = LastActivity.getLastActivity(con, "xray@jabber.org");
+ * </pre>
+ *
+ * To get the uptime of a host, you simple send the LastActivity packet to it,
+ * as in the following code example:
+ * <p>
+ *
+ * <pre>
+ * LastActivity activity = LastActivity.getLastActivity(con, "jabber.org");
+ * </pre>
+ *
+ * @author Gabriel Guardincerri
+ * @see <a href="http://xmpp.org/extensions/xep-0012.html">XEP-0012: Last
+ * Activity</a>
+ */
+
+public class LastActivityManager {
+
+ private long lastMessageSent;
+
+ private Connection connection;
+
+ // Enable the LastActivity support on every established connection
+ static {
+ Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+ public void connectionCreated(Connection connection) {
+ new LastActivityManager(connection);
+ }
+ });
+ }
+
+ /**
+ * Creates a last activity manager to response last activity requests.
+ *
+ * @param connection
+ * The Connection that the last activity requests will use.
+ */
+ private LastActivityManager(Connection connection) {
+ this.connection = connection;
+
+ // Listen to all the sent messages to reset the idle time on each one
+ connection.addPacketSendingListener(new PacketListener() {
+ public void processPacket(Packet packet) {
+ Presence presence = (Presence) packet;
+ Presence.Mode mode = presence.getMode();
+ if (mode == null) return;
+ switch (mode) {
+ case available:
+ case chat:
+ // We assume that only a switch to available and chat indicates user activity
+ // since other mode changes could be also a result of some sort of automatism
+ resetIdleTime();
+ }
+ }
+ }, new PacketTypeFilter(Presence.class));
+
+ connection.addPacketListener(new PacketListener() {
+ @Override
+ public void processPacket(Packet packet) {
+ Message message = (Message) packet;
+ // if it's not an error message, reset the idle time
+ if (message.getType() == Message.Type.error) return;
+ resetIdleTime();
+ }
+ }, new PacketTypeFilter(Message.class));
+
+ // Register a listener for a last activity query
+ connection.addPacketListener(new PacketListener() {
+
+ public void processPacket(Packet packet) {
+ LastActivity message = new LastActivity();
+ message.setType(IQ.Type.RESULT);
+ message.setTo(packet.getFrom());
+ message.setFrom(packet.getTo());
+ message.setPacketID(packet.getPacketID());
+ message.setLastActivity(getIdleTime());
+
+ LastActivityManager.this.connection.sendPacket(message);
+ }
+
+ }, new AndFilter(new IQTypeFilter(IQ.Type.GET), new PacketTypeFilter(LastActivity.class)));
+ ServiceDiscoveryManager.getInstanceFor(connection).addFeature(LastActivity.NAMESPACE);
+ resetIdleTime();
+ }
+
+ /**
+ * Resets the idle time to 0, this should be invoked when a new message is
+ * sent.
+ */
+ private void resetIdleTime() {
+ long now = System.currentTimeMillis();
+ synchronized (this) {
+ lastMessageSent = now;
+ }
+ }
+
+ /**
+ * The idle time is the lapsed time between the last message sent and now.
+ *
+ * @return the lapsed time between the last message sent and now.
+ */
+ private long getIdleTime() {
+ long lms;
+ long now = System.currentTimeMillis();
+ synchronized (this) {
+ lms = lastMessageSent;
+ }
+ return ((now - lms) / 1000);
+ }
+
+ /**
+ * Returns the last activity of a particular jid. If the jid is a full JID
+ * (i.e., a JID of the form of 'user@host/resource') then the last activity
+ * is the idle time of that connected resource. On the other hand, when the
+ * jid is a bare JID (e.g. 'user@host') then the last activity is the lapsed
+ * time since the last logout or 0 if the user is currently logged in.
+ * Moreover, when the jid is a server or component (e.g., a JID of the form
+ * 'host') the last activity is the uptime.
+ *
+ * @param con
+ * the current Connection.
+ * @param jid
+ * the JID of the user.
+ * @return the LastActivity packet of the jid.
+ * @throws XMPPException
+ * thrown if a server error has occured.
+ */
+ public static LastActivity getLastActivity(Connection con, String jid) throws XMPPException {
+ LastActivity activity = new LastActivity();
+ activity.setTo(jid);
+
+ PacketCollector collector = con.createPacketCollector(new PacketIDFilter(activity.getPacketID()));
+ con.sendPacket(activity);
+
+ LastActivity response = (LastActivity) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+ }
+
+ /**
+ * Returns true if Last Activity (XEP-0012) is supported by a given JID
+ *
+ * @param connection the connection to be used
+ * @param jid a JID to be tested for Last Activity support
+ * @return true if Last Activity is supported, otherwise false
+ */
+ public static boolean isLastActivitySupported(Connection connection, String jid) {
+ try {
+ DiscoverInfo result =
+ ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid);
+ return result.containsFeature(LastActivity.NAMESPACE);
+ }
+ catch (XMPPException e) {
+ return false;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/MessageEventManager.java b/src/org/jivesoftware/smackx/MessageEventManager.java new file mode 100644 index 0000000..3502509 --- /dev/null +++ b/src/org/jivesoftware/smackx/MessageEventManager.java @@ -0,0 +1,310 @@ +/** + * $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.smackx; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.filter.PacketExtensionFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.packet.MessageEvent; + +/** + * Manages message events requests and notifications. A MessageEventManager provides a high + * level access to request for notifications and send event notifications. It also provides + * an easy way to hook up custom logic when requests or notifications are received. + * + * @author Gaston Dombiak + */ +public class MessageEventManager { + + private List<MessageEventNotificationListener> messageEventNotificationListeners = new ArrayList<MessageEventNotificationListener>(); + private List<MessageEventRequestListener> messageEventRequestListeners = new ArrayList<MessageEventRequestListener>(); + + private Connection con; + + private PacketFilter packetFilter = new PacketExtensionFilter("x", "jabber:x:event"); + private PacketListener packetListener; + + /** + * Creates a new message event manager. + * + * @param con a Connection to a XMPP server. + */ + public MessageEventManager(Connection con) { + this.con = con; + init(); + } + + /** + * Adds event notification requests to a message. For each event type that + * the user wishes event notifications from the message recepient for, <tt>true</tt> + * should be passed in to this method. + * + * @param message the message to add the requested notifications. + * @param offline specifies if the offline event is requested. + * @param delivered specifies if the delivered event is requested. + * @param displayed specifies if the displayed event is requested. + * @param composing specifies if the composing event is requested. + */ + public static void addNotificationsRequests(Message message, boolean offline, + boolean delivered, boolean displayed, boolean composing) + { + // Create a MessageEvent Package and add it to the message + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setOffline(offline); + messageEvent.setDelivered(delivered); + messageEvent.setDisplayed(displayed); + messageEvent.setComposing(composing); + message.addExtension(messageEvent); + } + + /** + * Adds a message event request listener. The listener will be fired anytime a request for + * event notification is received. + * + * @param messageEventRequestListener a message event request listener. + */ + public void addMessageEventRequestListener(MessageEventRequestListener messageEventRequestListener) { + synchronized (messageEventRequestListeners) { + if (!messageEventRequestListeners.contains(messageEventRequestListener)) { + messageEventRequestListeners.add(messageEventRequestListener); + } + } + } + + /** + * Removes a message event request listener. The listener will be fired anytime a request for + * event notification is received. + * + * @param messageEventRequestListener a message event request listener. + */ + public void removeMessageEventRequestListener(MessageEventRequestListener messageEventRequestListener) { + synchronized (messageEventRequestListeners) { + messageEventRequestListeners.remove(messageEventRequestListener); + } + } + + /** + * Adds a message event notification listener. The listener will be fired anytime a notification + * event is received. + * + * @param messageEventNotificationListener a message event notification listener. + */ + public void addMessageEventNotificationListener(MessageEventNotificationListener messageEventNotificationListener) { + synchronized (messageEventNotificationListeners) { + if (!messageEventNotificationListeners.contains(messageEventNotificationListener)) { + messageEventNotificationListeners.add(messageEventNotificationListener); + } + } + } + + /** + * Removes a message event notification listener. The listener will be fired anytime a notification + * event is received. + * + * @param messageEventNotificationListener a message event notification listener. + */ + public void removeMessageEventNotificationListener(MessageEventNotificationListener messageEventNotificationListener) { + synchronized (messageEventNotificationListeners) { + messageEventNotificationListeners.remove(messageEventNotificationListener); + } + } + + /** + * Fires message event request listeners. + */ + private void fireMessageEventRequestListeners( + String from, + String packetID, + String methodName) { + MessageEventRequestListener[] listeners = null; + Method method; + synchronized (messageEventRequestListeners) { + listeners = new MessageEventRequestListener[messageEventRequestListeners.size()]; + messageEventRequestListeners.toArray(listeners); + } + try { + method = + MessageEventRequestListener.class.getDeclaredMethod( + methodName, + new Class[] { String.class, String.class, MessageEventManager.class }); + for (int i = 0; i < listeners.length; i++) { + method.invoke(listeners[i], new Object[] { from, packetID, this }); + } + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + /** + * Fires message event notification listeners. + */ + private void fireMessageEventNotificationListeners( + String from, + String packetID, + String methodName) { + MessageEventNotificationListener[] listeners = null; + Method method; + synchronized (messageEventNotificationListeners) { + listeners = + new MessageEventNotificationListener[messageEventNotificationListeners.size()]; + messageEventNotificationListeners.toArray(listeners); + } + try { + method = + MessageEventNotificationListener.class.getDeclaredMethod( + methodName, + new Class[] { String.class, String.class }); + for (int i = 0; i < listeners.length; i++) { + method.invoke(listeners[i], new Object[] { from, packetID }); + } + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + private void init() { + // Listens for all message event packets and fire the proper message event listeners. + packetListener = new PacketListener() { + public void processPacket(Packet packet) { + Message message = (Message) packet; + MessageEvent messageEvent = + (MessageEvent) message.getExtension("x", "jabber:x:event"); + if (messageEvent.isMessageEventRequest()) { + // Fire event for requests of message events + for (Iterator<String> it = messageEvent.getEventTypes(); it.hasNext();) + fireMessageEventRequestListeners( + message.getFrom(), + message.getPacketID(), + it.next().concat("NotificationRequested")); + } else + // Fire event for notifications of message events + for (Iterator<String> it = messageEvent.getEventTypes(); it.hasNext();) + fireMessageEventNotificationListeners( + message.getFrom(), + messageEvent.getPacketID(), + it.next().concat("Notification")); + + }; + + }; + con.addPacketListener(packetListener, packetFilter); + } + + /** + * Sends the notification that the message was delivered to the sender of the original message + * + * @param to the recipient of the notification. + * @param packetID the id of the message to send. + */ + public void sendDeliveredNotification(String to, String packetID) { + // Create the message to send + Message msg = new Message(to); + // Create a MessageEvent Package and add it to the message + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setDelivered(true); + messageEvent.setPacketID(packetID); + msg.addExtension(messageEvent); + // Send the packet + con.sendPacket(msg); + } + + /** + * Sends the notification that the message was displayed to the sender of the original message + * + * @param to the recipient of the notification. + * @param packetID the id of the message to send. + */ + public void sendDisplayedNotification(String to, String packetID) { + // Create the message to send + Message msg = new Message(to); + // Create a MessageEvent Package and add it to the message + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setDisplayed(true); + messageEvent.setPacketID(packetID); + msg.addExtension(messageEvent); + // Send the packet + con.sendPacket(msg); + } + + /** + * Sends the notification that the receiver of the message is composing a reply + * + * @param to the recipient of the notification. + * @param packetID the id of the message to send. + */ + public void sendComposingNotification(String to, String packetID) { + // Create the message to send + Message msg = new Message(to); + // Create a MessageEvent Package and add it to the message + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setComposing(true); + messageEvent.setPacketID(packetID); + msg.addExtension(messageEvent); + // Send the packet + con.sendPacket(msg); + } + + /** + * Sends the notification that the receiver of the message has cancelled composing a reply. + * + * @param to the recipient of the notification. + * @param packetID the id of the message to send. + */ + public void sendCancelledNotification(String to, String packetID) { + // Create the message to send + Message msg = new Message(to); + // Create a MessageEvent Package and add it to the message + MessageEvent messageEvent = new MessageEvent(); + messageEvent.setCancelled(true); + messageEvent.setPacketID(packetID); + msg.addExtension(messageEvent); + // Send the packet + con.sendPacket(msg); + } + + public void destroy() { + if (con != null) { + con.removePacketListener(packetListener); + } + } + + protected void finalize() throws Throwable { + destroy(); + super.finalize(); + } +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/MessageEventNotificationListener.java b/src/org/jivesoftware/smackx/MessageEventNotificationListener.java new file mode 100644 index 0000000..335dae2 --- /dev/null +++ b/src/org/jivesoftware/smackx/MessageEventNotificationListener.java @@ -0,0 +1,74 @@ +/** + * $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.smackx; + +/** + * + * A listener that is fired anytime a message event notification is received. + * Message event notifications are received as a consequence of the request + * to receive notifications when sending a message. + * + * @author Gaston Dombiak + */ +public interface MessageEventNotificationListener { + + /** + * Called when a notification of message delivered is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + */ + public void deliveredNotification(String from, String packetID); + + /** + * Called when a notification of message displayed is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + */ + public void displayedNotification(String from, String packetID); + + /** + * Called when a notification that the receiver of the message is composing a reply is + * received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + */ + public void composingNotification(String from, String packetID); + + /** + * Called when a notification that the receiver of the message is offline is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + */ + public void offlineNotification(String from, String packetID); + + /** + * Called when a notification that the receiver of the message cancelled the reply + * is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + */ + public void cancelledNotification(String from, String packetID); +} diff --git a/src/org/jivesoftware/smackx/MessageEventRequestListener.java b/src/org/jivesoftware/smackx/MessageEventRequestListener.java new file mode 100644 index 0000000..86e0808 --- /dev/null +++ b/src/org/jivesoftware/smackx/MessageEventRequestListener.java @@ -0,0 +1,86 @@ +/** + * $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.smackx; + +/** + * + * A listener that is fired anytime a message event request is received. + * Message event requests are received when the received message includes an extension + * like this: + * + * <pre> + * <x xmlns='jabber:x:event'> + * <offline/> + * <delivered/> + * <composing/> + * </x> + * </pre> + * + * In this example you can see that the sender of the message requests to be notified + * when the user couldn't receive the message because he/she is offline, the message + * was delivered or when the receiver of the message is composing a reply. + * + * @author Gaston Dombiak + */ +public interface MessageEventRequestListener { + + /** + * Called when a request for message delivered notification is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + * @param messageEventManager the messageEventManager that fired the listener. + */ + public void deliveredNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager); + + /** + * Called when a request for message displayed notification is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + * @param messageEventManager the messageEventManager that fired the listener. + */ + public void displayedNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager); + + /** + * Called when a request that the receiver of the message is composing a reply notification is + * received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + * @param messageEventManager the messageEventManager that fired the listener. + */ + public void composingNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager); + + /** + * Called when a request that the receiver of the message is offline is received. + * + * @param from the user that sent the notification. + * @param packetID the id of the message that was sent. + * @param messageEventManager the messageEventManager that fired the listener. + */ + public void offlineNotificationRequested(String from, String packetID, + MessageEventManager messageEventManager); + +} diff --git a/src/org/jivesoftware/smackx/MultipleRecipientInfo.java b/src/org/jivesoftware/smackx/MultipleRecipientInfo.java new file mode 100644 index 0000000..8738319 --- /dev/null +++ b/src/org/jivesoftware/smackx/MultipleRecipientInfo.java @@ -0,0 +1,98 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx; + +import org.jivesoftware.smackx.packet.MultipleAddresses; + +import java.util.List; + +/** + * MultipleRecipientInfo keeps information about the multiple recipients extension included + * in a received packet. Among the information we can find the list of TO and CC addresses. + * + * @author Gaston Dombiak + */ +public class MultipleRecipientInfo { + + MultipleAddresses extension; + + MultipleRecipientInfo(MultipleAddresses extension) { + this.extension = extension; + } + + /** + * Returns the list of {@link org.jivesoftware.smackx.packet.MultipleAddresses.Address} + * that were the primary recipients of the packet. + * + * @return list of primary recipients of the packet. + */ + public List<MultipleAddresses.Address> getTOAddresses() { + return extension.getAddressesOfType(MultipleAddresses.TO); + } + + /** + * Returns the list of {@link org.jivesoftware.smackx.packet.MultipleAddresses.Address} + * that were the secondary recipients of the packet. + * + * @return list of secondary recipients of the packet. + */ + public List<MultipleAddresses.Address> getCCAddresses() { + return extension.getAddressesOfType(MultipleAddresses.CC); + } + + /** + * Returns the JID of a MUC room to which responses should be sent or <tt>null</tt> if + * no specific address was provided. When no specific address was provided then the reply + * can be sent to any or all recipients. Otherwise, the user should join the specified room + * and send the reply to the room. + * + * @return the JID of a MUC room to which responses should be sent or <tt>null</tt> if + * no specific address was provided. + */ + public String getReplyRoom() { + List<MultipleAddresses.Address> replyRoom = extension.getAddressesOfType(MultipleAddresses.REPLY_ROOM); + return replyRoom.isEmpty() ? null : ((MultipleAddresses.Address) replyRoom.get(0)).getJid(); + } + + /** + * Returns true if the received packet should not be replied. Use + * {@link MultipleRecipientManager#reply(org.jivesoftware.smack.Connection, org.jivesoftware.smack.packet.Message, org.jivesoftware.smack.packet.Message)} + * to send replies. + * + * @return true if the received packet should not be replied. + */ + public boolean shouldNotReply() { + return !extension.getAddressesOfType(MultipleAddresses.NO_REPLY).isEmpty(); + } + + /** + * Returns the address to which all replies are requested to be sent or <tt>null</tt> if + * no specific address was provided. When no specific address was provided then the reply + * can be sent to any or all recipients. + * + * @return the address to which all replies are requested to be sent or <tt>null</tt> if + * no specific address was provided. + */ + public MultipleAddresses.Address getReplyAddress() { + List<MultipleAddresses.Address> replyTo = extension.getAddressesOfType(MultipleAddresses.REPLY_TO); + return replyTo.isEmpty() ? null : (MultipleAddresses.Address) replyTo.get(0); + } +} diff --git a/src/org/jivesoftware/smackx/MultipleRecipientManager.java b/src/org/jivesoftware/smackx/MultipleRecipientManager.java new file mode 100644 index 0000000..83aface --- /dev/null +++ b/src/org/jivesoftware/smackx/MultipleRecipientManager.java @@ -0,0 +1,353 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.util.Cache; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverItems; +import org.jivesoftware.smackx.packet.MultipleAddresses; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * A MultipleRecipientManager allows to send packets to multiple recipients by making use of + * <a href="http://www.jabber.org/jeps/jep-0033.html">JEP-33: Extended Stanza Addressing</a>. + * It also allows to send replies to packets that were sent to multiple recipients. + * + * @author Gaston Dombiak + */ +public class MultipleRecipientManager { + + /** + * Create a cache to hold the 100 most recently accessed elements for a period of + * 24 hours. + */ + private static Cache<String, String> services = new Cache<String, String>(100, 24 * 60 * 60 * 1000); + + /** + * Sends the specified packet to the list of specified recipients using the + * specified connection. If the server has support for JEP-33 then only one + * packet is going to be sent to the server with the multiple recipient instructions. + * However, if JEP-33 is not supported by the server then the client is going to send + * the packet to each recipient. + * + * @param connection the connection to use to send the packet. + * @param packet the packet to send to the list of recipients. + * @param to the list of JIDs to include in the TO list or <tt>null</tt> if no TO + * list exists. + * @param cc the list of JIDs to include in the CC list or <tt>null</tt> if no CC + * list exists. + * @param bcc the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC + * list exists. + * @throws XMPPException if server does not support JEP-33: Extended Stanza Addressing and + * some JEP-33 specific features were requested. + */ + public static void send(Connection connection, Packet packet, List<String> to, List<String> cc, List<String> bcc) + throws XMPPException { + send(connection, packet, to, cc, bcc, null, null, false); + } + + /** + * Sends the specified packet to the list of specified recipients using the + * specified connection. If the server has support for JEP-33 then only one + * packet is going to be sent to the server with the multiple recipient instructions. + * However, if JEP-33 is not supported by the server then the client is going to send + * the packet to each recipient. + * + * @param connection the connection to use to send the packet. + * @param packet the packet to send to the list of recipients. + * @param to the list of JIDs to include in the TO list or <tt>null</tt> if no TO + * list exists. + * @param cc the list of JIDs to include in the CC list or <tt>null</tt> if no CC + * list exists. + * @param bcc the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC + * list exists. + * @param replyTo address to which all replies are requested to be sent or <tt>null</tt> + * indicating that they can reply to any address. + * @param replyRoom JID of a MUC room to which responses should be sent or <tt>null</tt> + * indicating that they can reply to any address. + * @param noReply true means that receivers should not reply to the message. + * @throws XMPPException if server does not support JEP-33: Extended Stanza Addressing and + * some JEP-33 specific features were requested. + */ + public static void send(Connection connection, Packet packet, List<String> to, List<String> cc, List<String> bcc, + String replyTo, String replyRoom, boolean noReply) throws XMPPException { + String serviceAddress = getMultipleRecipienServiceAddress(connection); + if (serviceAddress != null) { + // Send packet to target users using multiple recipient service provided by the server + sendThroughService(connection, packet, to, cc, bcc, replyTo, replyRoom, noReply, + serviceAddress); + } + else { + // Server does not support JEP-33 so try to send the packet to each recipient + if (noReply || (replyTo != null && replyTo.trim().length() > 0) || + (replyRoom != null && replyRoom.trim().length() > 0)) { + // Some specified JEP-33 features were requested so throw an exception alerting + // the user that this features are not available + throw new XMPPException("Extended Stanza Addressing not supported by server"); + } + // Send the packet to each individual recipient + sendToIndividualRecipients(connection, packet, to, cc, bcc); + } + } + + /** + * Sends a reply to a previously received packet that was sent to multiple recipients. Before + * attempting to send the reply message some checkings are performed. If any of those checkings + * fail then an XMPPException is going to be thrown with the specific error detail. + * + * @param connection the connection to use to send the reply. + * @param original the previously received packet that was sent to multiple recipients. + * @param reply the new message to send as a reply. + * @throws XMPPException if the original message was not sent to multiple recipients, or the + * original message cannot be replied or reply should be sent to a room. + */ + public static void reply(Connection connection, Message original, Message reply) + throws XMPPException { + MultipleRecipientInfo info = getMultipleRecipientInfo(original); + if (info == null) { + throw new XMPPException("Original message does not contain multiple recipient info"); + } + if (info.shouldNotReply()) { + throw new XMPPException("Original message should not be replied"); + } + if (info.getReplyRoom() != null) { + throw new XMPPException("Reply should be sent through a room"); + } + // Any <thread/> element from the initial message MUST be copied into the reply. + if (original.getThread() != null) { + reply.setThread(original.getThread()); + } + MultipleAddresses.Address replyAddress = info.getReplyAddress(); + if (replyAddress != null && replyAddress.getJid() != null) { + // Send reply to the reply_to address + reply.setTo(replyAddress.getJid()); + connection.sendPacket(reply); + } + else { + // Send reply to multiple recipients + List<String> to = new ArrayList<String>(); + List<String> cc = new ArrayList<String>(); + for (Iterator<MultipleAddresses.Address> it = info.getTOAddresses().iterator(); it.hasNext();) { + String jid = it.next().getJid(); + to.add(jid); + } + for (Iterator<MultipleAddresses.Address> it = info.getCCAddresses().iterator(); it.hasNext();) { + String jid = it.next().getJid(); + cc.add(jid); + } + // Add original sender as a 'to' address (if not already present) + if (!to.contains(original.getFrom()) && !cc.contains(original.getFrom())) { + to.add(original.getFrom()); + } + // Remove the sender from the TO/CC list (try with bare JID too) + String from = connection.getUser(); + if (!to.remove(from) && !cc.remove(from)) { + String bareJID = StringUtils.parseBareAddress(from); + to.remove(bareJID); + cc.remove(bareJID); + } + + String serviceAddress = getMultipleRecipienServiceAddress(connection); + if (serviceAddress != null) { + // Send packet to target users using multiple recipient service provided by the server + sendThroughService(connection, reply, to, cc, null, null, null, false, + serviceAddress); + } + else { + // Server does not support JEP-33 so try to send the packet to each recipient + sendToIndividualRecipients(connection, reply, to, cc, null); + } + } + } + + /** + * Returns the {@link MultipleRecipientInfo} contained in the specified packet or + * <tt>null</tt> if none was found. Only packets sent to multiple recipients will + * contain such information. + * + * @param packet the packet to check. + * @return the MultipleRecipientInfo contained in the specified packet or <tt>null</tt> + * if none was found. + */ + public static MultipleRecipientInfo getMultipleRecipientInfo(Packet packet) { + MultipleAddresses extension = (MultipleAddresses) packet + .getExtension("addresses", "http://jabber.org/protocol/address"); + return extension == null ? null : new MultipleRecipientInfo(extension); + } + + private static void sendToIndividualRecipients(Connection connection, Packet packet, + List<String> to, List<String> cc, List<String> bcc) { + if (to != null) { + for (Iterator<String> it = to.iterator(); it.hasNext();) { + String jid = it.next(); + packet.setTo(jid); + connection.sendPacket(new PacketCopy(packet.toXML())); + } + } + if (cc != null) { + for (Iterator<String> it = cc.iterator(); it.hasNext();) { + String jid = it.next(); + packet.setTo(jid); + connection.sendPacket(new PacketCopy(packet.toXML())); + } + } + if (bcc != null) { + for (Iterator<String> it = bcc.iterator(); it.hasNext();) { + String jid = it.next(); + packet.setTo(jid); + connection.sendPacket(new PacketCopy(packet.toXML())); + } + } + } + + private static void sendThroughService(Connection connection, Packet packet, List<String> to, + List<String> cc, List<String> bcc, String replyTo, String replyRoom, boolean noReply, + String serviceAddress) { + // Create multiple recipient extension + MultipleAddresses multipleAddresses = new MultipleAddresses(); + if (to != null) { + for (Iterator<String> it = to.iterator(); it.hasNext();) { + String jid = it.next(); + multipleAddresses.addAddress(MultipleAddresses.TO, jid, null, null, false, null); + } + } + if (cc != null) { + for (Iterator<String> it = cc.iterator(); it.hasNext();) { + String jid = it.next(); + multipleAddresses.addAddress(MultipleAddresses.CC, jid, null, null, false, null); + } + } + if (bcc != null) { + for (Iterator<String> it = bcc.iterator(); it.hasNext();) { + String jid = it.next(); + multipleAddresses.addAddress(MultipleAddresses.BCC, jid, null, null, false, null); + } + } + if (noReply) { + multipleAddresses.setNoReply(); + } + else { + if (replyTo != null && replyTo.trim().length() > 0) { + multipleAddresses + .addAddress(MultipleAddresses.REPLY_TO, replyTo, null, null, false, null); + } + if (replyRoom != null && replyRoom.trim().length() > 0) { + multipleAddresses.addAddress(MultipleAddresses.REPLY_ROOM, replyRoom, null, null, + false, null); + } + } + // Set the multiple recipient service address as the target address + packet.setTo(serviceAddress); + // Add extension to packet + packet.addExtension(multipleAddresses); + // Send the packet + connection.sendPacket(packet); + } + + /** + * Returns the address of the multiple recipients service. To obtain such address service + * discovery is going to be used on the connected server and if none was found then another + * attempt will be tried on the server items. The discovered information is going to be + * cached for 24 hours. + * + * @param connection the connection to use for disco. The connected server is going to be + * queried. + * @return the address of the multiple recipients service or <tt>null</tt> if none was found. + */ + private static String getMultipleRecipienServiceAddress(Connection connection) { + String serviceName = connection.getServiceName(); + String serviceAddress = (String) services.get(serviceName); + if (serviceAddress == null) { + synchronized (services) { + serviceAddress = (String) services.get(serviceName); + if (serviceAddress == null) { + + // Send the disco packet to the server itself + try { + DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection) + .discoverInfo(serviceName); + // Check if the server supports JEP-33 + if (info.containsFeature("http://jabber.org/protocol/address")) { + serviceAddress = serviceName; + } + else { + // Get the disco items and send the disco packet to each server item + DiscoverItems items = ServiceDiscoveryManager.getInstanceFor(connection) + .discoverItems(serviceName); + for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) { + DiscoverItems.Item item = it.next(); + info = ServiceDiscoveryManager.getInstanceFor(connection) + .discoverInfo(item.getEntityID(), item.getNode()); + if (info.containsFeature("http://jabber.org/protocol/address")) { + serviceAddress = serviceName; + break; + } + } + + } + // Cache the discovered information + services.put(serviceName, serviceAddress == null ? "" : serviceAddress); + } + catch (XMPPException e) { + e.printStackTrace(); + } + } + } + } + + return "".equals(serviceAddress) ? null : serviceAddress; + } + + /** + * Packet that holds the XML stanza to send. This class is useful when the same packet + * is needed to be sent to different recipients. Since using the same packet is not possible + * (i.e. cannot change the TO address of a queues packet to be sent) then this class was + * created to keep the XML stanza to send. + */ + private static class PacketCopy extends Packet { + + private String text; + + /** + * Create a copy of a packet with the text to send. The passed text must be a valid text to + * send to the server, no validation will be done on the passed text. + * + * @param text the whole text of the packet to send + */ + public PacketCopy(String text) { + this.text = text; + } + + public String toXML() { + return text; + } + + } + +} diff --git a/src/org/jivesoftware/smackx/NodeInformationProvider.java b/src/org/jivesoftware/smackx/NodeInformationProvider.java new file mode 100644 index 0000000..27ee53a --- /dev/null +++ b/src/org/jivesoftware/smackx/NodeInformationProvider.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.smackx; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverItems; + +import java.util.List; + + +/** + * The NodeInformationProvider is responsible for providing supported indentities, features + * and hosted items (i.e. DiscoverItems.Item) about a given node. This information will be + * requested each time this XMPPP client receives a disco info or items requests on the + * given node. each time this XMPPP client receives a disco info or items requests on the + * given node. + * + * @author Gaston Dombiak + */ +public interface NodeInformationProvider { + + /** + * Returns a list of the Items {@link org.jivesoftware.smackx.packet.DiscoverItems.Item} + * defined in the node. For example, the MUC protocol specifies that an XMPP client should + * answer an Item for each joined room when asked for the rooms where the use has joined. + * + * @return a list of the Items defined in the node. + */ + List<DiscoverItems.Item> getNodeItems(); + + /** + * Returns a list of the features defined in the node. For + * example, the entity caps protocol specifies that an XMPP client + * should answer with each feature supported by the client version + * or extension. + * + * @return a list of the feature strings defined in the node. + */ + List<String> getNodeFeatures(); + + /** + * Returns a list of the indentites defined in the node. For + * example, the x-command protocol must provide an identity of + * category automation and type command-node for each command. + * + * @return a list of the Identities defined in the node. + */ + List<DiscoverInfo.Identity> getNodeIdentities(); + + /** + * Returns a list of the packet extensions defined in the node. + * + * @return a list of the packet extensions defined in the node. + */ + List<PacketExtension> getNodePacketExtensions(); +} diff --git a/src/org/jivesoftware/smackx/OfflineMessageHeader.java b/src/org/jivesoftware/smackx/OfflineMessageHeader.java new file mode 100644 index 0000000..55fd149 --- /dev/null +++ b/src/org/jivesoftware/smackx/OfflineMessageHeader.java @@ -0,0 +1,85 @@ +/** + * $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.smackx; + +import org.jivesoftware.smackx.packet.DiscoverItems; + +/** + * The OfflineMessageHeader holds header information of an offline message. The header + * information was retrieved using the {@link OfflineMessageManager} class.<p> + * + * Each offline message is identified by the target user of the offline message and a unique stamp. + * Use {@link OfflineMessageManager#getMessages(java.util.List)} to retrieve the whole message. + * + * @author Gaston Dombiak + */ +public class OfflineMessageHeader { + /** + * Bare JID of the user that was offline when the message was sent. + */ + private String user; + /** + * Full JID of the user that sent the message. + */ + private String jid; + /** + * Stamp that uniquely identifies the offline message. This stamp will be used for + * getting the specific message or delete it. The stamp may be of the form UTC timestamps + * but it is not required to have that format. + */ + private String stamp; + + public OfflineMessageHeader(DiscoverItems.Item item) { + super(); + user = item.getEntityID(); + jid = item.getName(); + stamp = item.getNode(); + } + + /** + * Returns the bare JID of the user that was offline when the message was sent. + * + * @return the bare JID of the user that was offline when the message was sent. + */ + public String getUser() { + return user; + } + + /** + * Returns the full JID of the user that sent the message. + * + * @return the full JID of the user that sent the message. + */ + public String getJid() { + return jid; + } + + /** + * Returns the stamp that uniquely identifies the offline message. This stamp will + * be used for getting the specific message or delete it. The stamp may be of the + * form UTC timestamps but it is not required to have that format. + * + * @return the stamp that uniquely identifies the offline message. + */ + public String getStamp() { + return stamp; + } +} diff --git a/src/org/jivesoftware/smackx/OfflineMessageManager.java b/src/org/jivesoftware/smackx/OfflineMessageManager.java new file mode 100644 index 0000000..dbe889d --- /dev/null +++ b/src/org/jivesoftware/smackx/OfflineMessageManager.java @@ -0,0 +1,284 @@ +/** + * $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.smackx; + +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.*; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverItems; +import org.jivesoftware.smackx.packet.OfflineMessageInfo; +import org.jivesoftware.smackx.packet.OfflineMessageRequest; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * The OfflineMessageManager helps manage offline messages even before the user has sent an + * available presence. When a user asks for his offline messages before sending an available + * presence then the server will not send a flood with all the offline messages when the user + * becomes online. The server will not send a flood with all the offline messages to the session + * that made the offline messages request or to any other session used by the user that becomes + * online.<p> + * + * Once the session that made the offline messages request has been closed and the user becomes + * offline in all the resources then the server will resume storing the messages offline and will + * send all the offline messages to the user when he becomes online. Therefore, the server will + * flood the user when he becomes online unless the user uses this class to manage his offline + * messages. + * + * @author Gaston Dombiak + */ +public class OfflineMessageManager { + + private final static String namespace = "http://jabber.org/protocol/offline"; + + private Connection connection; + + private PacketFilter packetFilter; + + public OfflineMessageManager(Connection connection) { + this.connection = connection; + packetFilter = + new AndFilter(new PacketExtensionFilter("offline", namespace), + new PacketTypeFilter(Message.class)); + } + + /** + * Returns true if the server supports Flexible Offline Message Retrieval. When the server + * supports Flexible Offline Message Retrieval it is possible to get the header of the offline + * messages, get specific messages, delete specific messages, etc. + * + * @return a boolean indicating if the server supports Flexible Offline Message Retrieval. + * @throws XMPPException If the user is not allowed to make this request. + */ + public boolean supportsFlexibleRetrieval() throws XMPPException { + DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(connection.getServiceName()); + return info.containsFeature(namespace); + } + + /** + * Returns the number of offline messages for the user of the connection. + * + * @return the number of offline messages for the user of the connection. + * @throws XMPPException If the user is not allowed to make this request or the server does + * not support offline message retrieval. + */ + public int getMessageCount() throws XMPPException { + DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(null, + namespace); + Form extendedInfo = Form.getFormFrom(info); + if (extendedInfo != null) { + String value = extendedInfo.getField("number_of_messages").getValues().next(); + return Integer.parseInt(value); + } + return 0; + } + + /** + * Returns an iterator on <tt>OfflineMessageHeader</tt> that keep information about the + * offline message. The OfflineMessageHeader includes a stamp that could be used to retrieve + * the complete message or delete the specific message. + * + * @return an iterator on <tt>OfflineMessageHeader</tt> that keep information about the offline + * message. + * @throws XMPPException If the user is not allowed to make this request or the server does + * not support offline message retrieval. + */ + public Iterator<OfflineMessageHeader> getHeaders() throws XMPPException { + List<OfflineMessageHeader> answer = new ArrayList<OfflineMessageHeader>(); + DiscoverItems items = ServiceDiscoveryManager.getInstanceFor(connection).discoverItems( + null, namespace); + for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) { + DiscoverItems.Item item = it.next(); + answer.add(new OfflineMessageHeader(item)); + } + return answer.iterator(); + } + + /** + * Returns an Iterator with the offline <tt>Messages</tt> whose stamp matches the specified + * request. The request will include the list of stamps that uniquely identifies + * the offline messages to retrieve. The returned offline messages will not be deleted + * from the server. Use {@link #deleteMessages(java.util.List)} to delete the messages. + * + * @param nodes the list of stamps that uniquely identifies offline message. + * @return an Iterator with the offline <tt>Messages</tt> that were received as part of + * this request. + * @throws XMPPException If the user is not allowed to make this request or the server does + * not support offline message retrieval. + */ + public Iterator<Message> getMessages(final List<String> nodes) throws XMPPException { + List<Message> messages = new ArrayList<Message>(); + OfflineMessageRequest request = new OfflineMessageRequest(); + for (String node : nodes) { + OfflineMessageRequest.Item item = new OfflineMessageRequest.Item(node); + item.setAction("view"); + request.addItem(item); + } + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(request.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Filter offline messages that were requested by this request + PacketFilter messageFilter = new AndFilter(packetFilter, new PacketFilter() { + public boolean accept(Packet packet) { + OfflineMessageInfo info = (OfflineMessageInfo) packet.getExtension("offline", + namespace); + return nodes.contains(info.getNode()); + } + }); + PacketCollector messageCollector = connection.createPacketCollector(messageFilter); + // Send the retrieval request to the server. + connection.sendPacket(request); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + + // Collect the received offline messages + Message message = (Message) messageCollector.nextResult( + SmackConfiguration.getPacketReplyTimeout()); + while (message != null) { + messages.add(message); + message = + (Message) messageCollector.nextResult( + SmackConfiguration.getPacketReplyTimeout()); + } + // Stop queuing offline messages + messageCollector.cancel(); + return messages.iterator(); + } + + /** + * Returns an Iterator with all the offline <tt>Messages</tt> of the user. The returned offline + * messages will not be deleted from the server. Use {@link #deleteMessages(java.util.List)} + * to delete the messages. + * + * @return an Iterator with all the offline <tt>Messages</tt> of the user. + * @throws XMPPException If the user is not allowed to make this request or the server does + * not support offline message retrieval. + */ + public Iterator<Message> getMessages() throws XMPPException { + List<Message> messages = new ArrayList<Message>(); + OfflineMessageRequest request = new OfflineMessageRequest(); + request.setFetch(true); + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(request.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Filter offline messages that were requested by this request + PacketCollector messageCollector = connection.createPacketCollector(packetFilter); + // Send the retrieval request to the server. + connection.sendPacket(request); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + + // Collect the received offline messages + Message message = (Message) messageCollector.nextResult( + SmackConfiguration.getPacketReplyTimeout()); + while (message != null) { + messages.add(message); + message = + (Message) messageCollector.nextResult( + SmackConfiguration.getPacketReplyTimeout()); + } + // Stop queuing offline messages + messageCollector.cancel(); + return messages.iterator(); + } + + /** + * Deletes the specified list of offline messages. The request will include the list of + * stamps that uniquely identifies the offline messages to delete. + * + * @param nodes the list of stamps that uniquely identifies offline message. + * @throws XMPPException If the user is not allowed to make this request or the server does + * not support offline message retrieval. + */ + public void deleteMessages(List<String> nodes) throws XMPPException { + OfflineMessageRequest request = new OfflineMessageRequest(); + for (String node : nodes) { + OfflineMessageRequest.Item item = new OfflineMessageRequest.Item(node); + item.setAction("remove"); + request.addItem(item); + } + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(request.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the deletion request to the server. + connection.sendPacket(request); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + /** + * Deletes all offline messages of the user. + * + * @throws XMPPException If the user is not allowed to make this request or the server does + * not support offline message retrieval. + */ + public void deleteMessages() throws XMPPException { + OfflineMessageRequest request = new OfflineMessageRequest(); + request.setPurge(true); + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(request.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the deletion request to the server. + connection.sendPacket(request); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } +} diff --git a/src/org/jivesoftware/smackx/PEPListener.java b/src/org/jivesoftware/smackx/PEPListener.java new file mode 100644 index 0000000..1d39484 --- /dev/null +++ b/src/org/jivesoftware/smackx/PEPListener.java @@ -0,0 +1,42 @@ +/** + * $RCSfile: PEPListener.java,v $ + * $Revision: 1.1 $ + * $Date: 2007/11/03 00:14:32 $ + * + * 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.smackx; + +import org.jivesoftware.smackx.packet.PEPEvent; + + +/** + * + * A listener that is fired anytime a PEP event message is received. + * + * @author Jeff Williams + */ +public interface PEPListener { + + /** + * Called when PEP events are received as part of a presence subscribe or message filter. + * + * @param from the user that sent the entries. + * @param event the event contained in the message. + */ + public void eventReceived(String from, PEPEvent event); + +} diff --git a/src/org/jivesoftware/smackx/PEPManager.java b/src/org/jivesoftware/smackx/PEPManager.java new file mode 100644 index 0000000..857f1c4 --- /dev/null +++ b/src/org/jivesoftware/smackx/PEPManager.java @@ -0,0 +1,160 @@ +/** + * $RCSfile: PEPManager.java,v $ + * $Revision: 1.4 $ + * $Date: 2007/11/06 21:43:40 $ + * + * 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.smackx; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.filter.PacketExtensionFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.IQ.Type; +import org.jivesoftware.smackx.packet.PEPEvent; +import org.jivesoftware.smackx.packet.PEPItem; +import org.jivesoftware.smackx.packet.PEPPubSub; + +/** + * + * Manages Personal Event Publishing (XEP-163). A PEPManager provides a high level access to + * pubsub personal events. It also provides an easy way + * to hook up custom logic when events are received from another XMPP client through PEPListeners. + * + * Use example: + * + * <pre> + * PEPManager pepManager = new PEPManager(smackConnection); + * pepManager.addPEPListener(new PEPListener() { + * public void eventReceived(String inFrom, PEPEvent inEvent) { + * LOGGER.debug("Event received: " + inEvent); + * } + * }); + * + * PEPProvider pepProvider = new PEPProvider(); + * pepProvider.registerPEPParserExtension("http://jabber.org/protocol/tune", new TuneProvider()); + * ProviderManager.getInstance().addExtensionProvider("event", "http://jabber.org/protocol/pubsub#event", pepProvider); + * + * Tune tune = new Tune("jeff", "1", "CD", "My Title", "My Track"); + * pepManager.publish(tune); + * </pre> + * + * @author Jeff Williams + */ +public class PEPManager { + + private List<PEPListener> pepListeners = new ArrayList<PEPListener>(); + + private Connection connection; + + private PacketFilter packetFilter = new PacketExtensionFilter("event", "http://jabber.org/protocol/pubsub#event"); + private PacketListener packetListener; + + /** + * Creates a new PEP exchange manager. + * + * @param connection a Connection which is used to send and receive messages. + */ + public PEPManager(Connection connection) { + this.connection = connection; + init(); + } + + /** + * Adds a listener to PEPs. The listener will be fired anytime PEP events + * are received from remote XMPP clients. + * + * @param pepListener a roster exchange listener. + */ + public void addPEPListener(PEPListener pepListener) { + synchronized (pepListeners) { + if (!pepListeners.contains(pepListener)) { + pepListeners.add(pepListener); + } + } + } + + /** + * Removes a listener from PEP events. + * + * @param pepListener a roster exchange listener. + */ + public void removePEPListener(PEPListener pepListener) { + synchronized (pepListeners) { + pepListeners.remove(pepListener); + } + } + + /** + * Publish an event. + * + * @param item the item to publish. + */ + public void publish(PEPItem item) { + // Create a new message to publish the event. + PEPPubSub pubSub = new PEPPubSub(item); + pubSub.setType(Type.SET); + //pubSub.setFrom(connection.getUser()); + + // Send the message that contains the roster + connection.sendPacket(pubSub); + } + + /** + * Fires roster exchange listeners. + */ + private void firePEPListeners(String from, PEPEvent event) { + PEPListener[] listeners = null; + synchronized (pepListeners) { + listeners = new PEPListener[pepListeners.size()]; + pepListeners.toArray(listeners); + } + for (int i = 0; i < listeners.length; i++) { + listeners[i].eventReceived(from, event); + } + } + + private void init() { + // Listens for all roster exchange packets and fire the roster exchange listeners. + packetListener = new PacketListener() { + public void processPacket(Packet packet) { + Message message = (Message) packet; + PEPEvent event = (PEPEvent) message.getExtension("event", "http://jabber.org/protocol/pubsub#event"); + // Fire event for roster exchange listeners + firePEPListeners(message.getFrom(), event); + }; + + }; + connection.addPacketListener(packetListener, packetFilter); + } + + public void destroy() { + if (connection != null) + connection.removePacketListener(packetListener); + + } + + protected void finalize() throws Throwable { + destroy(); + super.finalize(); + } +} diff --git a/src/org/jivesoftware/smackx/PrivateDataManager.java b/src/org/jivesoftware/smackx/PrivateDataManager.java new file mode 100644 index 0000000..c6440bc --- /dev/null +++ b/src/org/jivesoftware/smackx/PrivateDataManager.java @@ -0,0 +1,359 @@ +/** + * $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.smackx; + +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.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.packet.DefaultPrivateData; +import org.jivesoftware.smackx.packet.PrivateData; +import org.jivesoftware.smackx.provider.PrivateDataProvider; +import org.xmlpull.v1.XmlPullParser; + +import java.util.Hashtable; +import java.util.Map; + +/** + * Manages private data, which is a mechanism to allow users to store arbitrary XML + * data on an XMPP server. Each private data chunk is defined by a element name and + * XML namespace. Example private data: + * + * <pre> + * <color xmlns="http://example.com/xmpp/color"> + * <favorite>blue</blue> + * <leastFavorite>puce</leastFavorite> + * </color> + * </pre> + * + * {@link PrivateDataProvider} instances are responsible for translating the XML into objects. + * If no PrivateDataProvider is registered for a given element name and namespace, then + * a {@link DefaultPrivateData} instance will be returned.<p> + * + * Warning: this is an non-standard protocol documented by + * <a href="http://www.jabber.org/jeps/jep-0049.html">JEP-49</a>. Because this is a + * non-standard protocol, it is subject to change. + * + * @author Matt Tucker + */ +public class PrivateDataManager { + + /** + * Map of provider instances. + */ + private static Map<String, PrivateDataProvider> privateDataProviders = new Hashtable<String, PrivateDataProvider>(); + + /** + * Returns the private data provider registered to the specified XML element name and namespace. + * For example, if a provider was registered to the element name "prefs" and the + * namespace "http://www.xmppclient.com/prefs", then the following packet would trigger + * the provider: + * + * <pre> + * <iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'> + * <query xmlns='jabber:iq:private'> + * <prefs xmlns='http://www.xmppclient.com/prefs'> + * <value1>ABC</value1> + * <value2>XYZ</value2> + * </prefs> + * </query> + * </iq></pre> + * + * <p>Note: this method is generally only called by the internal Smack classes. + * + * @param elementName the XML element name. + * @param namespace the XML namespace. + * @return the PrivateData provider. + */ + public static PrivateDataProvider getPrivateDataProvider(String elementName, String namespace) { + String key = getProviderKey(elementName, namespace); + return (PrivateDataProvider)privateDataProviders.get(key); + } + + /** + * Adds a private data provider with the specified element name and name space. The provider + * will override any providers loaded through the classpath. + * + * @param elementName the XML element name. + * @param namespace the XML namespace. + * @param provider the private data provider. + */ + public static void addPrivateDataProvider(String elementName, String namespace, + PrivateDataProvider provider) + { + String key = getProviderKey(elementName, namespace); + privateDataProviders.put(key, provider); + } + + /** + * Removes a private data provider with the specified element name and namespace. + * + * @param elementName The XML element name. + * @param namespace The XML namespace. + */ + public static void removePrivateDataProvider(String elementName, String namespace) { + String key = getProviderKey(elementName, namespace); + privateDataProviders.remove(key); + } + + + private Connection connection; + + /** + * The user to get and set private data for. In most cases, this value should + * be <tt>null</tt>, as the typical use of private data is to get and set + * your own private data and not others. + */ + private String user; + + /** + * Creates a new private data manager. The connection must have + * undergone a successful login before being used to construct an instance of + * this class. + * + * @param connection an XMPP connection which must have already undergone a + * successful login. + */ + public PrivateDataManager(Connection connection) { + if (!connection.isAuthenticated()) { + throw new IllegalStateException("Must be logged in to XMPP server."); + } + this.connection = connection; + } + + /** + * Creates a new private data manager for a specific user (special case). Most + * servers only support getting and setting private data for the user that + * authenticated via the connection. However, some servers support the ability + * to get and set private data for other users (for example, if you are the + * administrator). The connection must have undergone a successful login before + * being used to construct an instance of this class. + * + * @param connection an XMPP connection which must have already undergone a + * successful login. + * @param user the XMPP address of the user to get and set private data for. + */ + public PrivateDataManager(Connection connection, String user) { + if (!connection.isAuthenticated()) { + throw new IllegalStateException("Must be logged in to XMPP server."); + } + this.connection = connection; + this.user = user; + } + + /** + * Returns the private data specified by the given element name and namespace. Each chunk + * of private data is uniquely identified by an element name and namespace pair.<p> + * + * If a PrivateDataProvider is registered for the specified element name/namespace pair then + * that provider will determine the specific object type that is returned. If no provider + * is registered, a {@link DefaultPrivateData} instance will be returned. + * + * @param elementName the element name. + * @param namespace the namespace. + * @return the private data. + * @throws XMPPException if an error occurs getting the private data. + */ + public PrivateData getPrivateData(final String elementName, final String namespace) + throws XMPPException + { + // Create an IQ packet to get the private data. + IQ privateDataGet = new IQ() { + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<query xmlns=\"jabber:iq:private\">"); + buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\"/>"); + buf.append("</query>"); + return buf.toString(); + } + }; + privateDataGet.setType(IQ.Type.GET); + // Address the packet to the other account if user has been set. + if (user != null) { + privateDataGet.setTo(user); + } + + // Setup a listener for the reply to the set operation. + String packetID = privateDataGet.getPacketID(); + PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID)); + + // Send the private data. + connection.sendPacket(privateDataGet); + + // Wait up to five seconds for a response from the server. + IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from the server."); + } + // If the server replied with an error, throw an exception. + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + return ((PrivateDataResult)response).getPrivateData(); + } + + /** + * Sets a private data value. Each chunk of private data is uniquely identified by an + * element name and namespace pair. If private data has already been set with the + * element name and namespace, then the new private data will overwrite the old value. + * + * @param privateData the private data. + * @throws XMPPException if setting the private data fails. + */ + public void setPrivateData(final PrivateData privateData) throws XMPPException { + // Create an IQ packet to set the private data. + IQ privateDataSet = new IQ() { + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<query xmlns=\"jabber:iq:private\">"); + buf.append(privateData.toXML()); + buf.append("</query>"); + return buf.toString(); + } + }; + privateDataSet.setType(IQ.Type.SET); + // Address the packet to the other account if user has been set. + if (user != null) { + privateDataSet.setTo(user); + } + + // Setup a listener for the reply to the set operation. + String packetID = privateDataSet.getPacketID(); + PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID)); + + // Send the private data. + connection.sendPacket(privateDataSet); + + // Wait up to five seconds for a response from the server. + IQ response = (IQ)collector.nextResult(5000); + // Stop queuing results + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from the server."); + } + // If the server replied with an error, throw an exception. + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + } + + /** + * Returns a String key for a given element name and namespace. + * + * @param elementName the element name. + * @param namespace the namespace. + * @return a unique key for the element name and namespace pair. + */ + private static String getProviderKey(String elementName, String namespace) { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(elementName).append("/><").append(namespace).append("/>"); + return buf.toString(); + } + + /** + * An IQ provider to parse IQ results containing private data. + */ + public static class PrivateDataIQProvider implements IQProvider { + public IQ parseIQ(XmlPullParser parser) throws Exception { + PrivateData privateData = null; + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + // See if any objects are registered to handle this private data type. + PrivateDataProvider provider = getPrivateDataProvider(elementName, namespace); + // If there is a registered provider, use it. + if (provider != null) { + privateData = provider.parsePrivateData(parser); + } + // Otherwise, use a DefaultPrivateData instance to store the private data. + else { + DefaultPrivateData data = new DefaultPrivateData(elementName, namespace); + boolean finished = false; + while (!finished) { + int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + String name = parser.getName(); + // If an empty element, set the value with the empty string. + if (parser.isEmptyElementTag()) { + data.setValue(name,""); + } + // Otherwise, get the the element text. + else { + event = parser.next(); + if (event == XmlPullParser.TEXT) { + String value = parser.getText(); + data.setValue(name, value); + } + } + } + else if (event == XmlPullParser.END_TAG) { + if (parser.getName().equals(elementName)) { + finished = true; + } + } + } + privateData = data; + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + return new PrivateDataResult(privateData); + } + } + + /** + * An IQ packet to hold PrivateData GET results. + */ + private static class PrivateDataResult extends IQ { + + private PrivateData privateData; + + PrivateDataResult(PrivateData privateData) { + this.privateData = privateData; + } + + public PrivateData getPrivateData() { + return privateData; + } + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<query xmlns=\"jabber:iq:private\">"); + if (privateData != null) { + privateData.toXML(); + } + buf.append("</query>"); + return buf.toString(); + } + } +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/RemoteRosterEntry.java b/src/org/jivesoftware/smackx/RemoteRosterEntry.java new file mode 100644 index 0000000..5df3690 --- /dev/null +++ b/src/org/jivesoftware/smackx/RemoteRosterEntry.java @@ -0,0 +1,114 @@ +/** + * $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.smackx; + +import java.util.*; + +/** + * Represents a roster item, which consists of a JID and , their name and + * the groups the roster item belongs to. This roster item does not belong + * to the local roster. Therefore, it does not persist in the server.<p> + * + * The idea of a RemoteRosterEntry is to be used as part of a roster exchange. + * + * @author Gaston Dombiak + */ +public class RemoteRosterEntry { + + private String user; + private String name; + private final List<String> groupNames = new ArrayList<String>(); + + /** + * Creates a new remote roster entry. + * + * @param user the user. + * @param name the user's name. + * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the + * the roster entry won't belong to a group. + */ + public RemoteRosterEntry(String user, String name, String [] groups) { + this.user = user; + this.name = name; + if (groups != null) { + groupNames.addAll(Arrays.asList(groups)); + } + } + + /** + * Returns the user. + * + * @return the user. + */ + public String getUser() { + return user; + } + + /** + * Returns the user's name. + * + * @return the user's name. + */ + public String getName() { + return name; + } + + /** + * Returns an Iterator for the group names (as Strings) that the roster entry + * belongs to. + * + * @return an Iterator for the group names. + */ + public Iterator<String> getGroupNames() { + synchronized (groupNames) { + return Collections.unmodifiableList(groupNames).iterator(); + } + } + + /** + * Returns a String array for the group names that the roster entry + * belongs to. + * + * @return a String[] for the group names. + */ + public String[] getGroupArrayNames() { + synchronized (groupNames) { + return Collections.unmodifiableList(groupNames).toArray(new String[groupNames.size()]); + } + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<item jid=\"").append(user).append("\""); + if (name != null) { + buf.append(" name=\"").append(name).append("\""); + } + buf.append(">"); + synchronized (groupNames) { + for (String groupName : groupNames) { + buf.append("<group>").append(groupName).append("</group>"); + } + } + buf.append("</item>"); + return buf.toString(); + } + +} diff --git a/src/org/jivesoftware/smackx/ReportedData.java b/src/org/jivesoftware/smackx/ReportedData.java new file mode 100644 index 0000000..0d7b760 --- /dev/null +++ b/src/org/jivesoftware/smackx/ReportedData.java @@ -0,0 +1,281 @@ +/** + * $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.smackx; + +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.packet.DataForm; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Represents a set of data results returned as part of a search. The report is structured + * in columns and rows. + * + * @author Gaston Dombiak + */ +public class ReportedData { + + private List<Column> columns = new ArrayList<Column>(); + private List<Row> rows = new ArrayList<Row>(); + private String title = ""; + + /** + * Returns a new ReportedData if the packet is used for reporting data and includes an + * extension that matches the elementName and namespace "x","jabber:x:data". + * + * @param packet the packet used for reporting data. + */ + public static ReportedData getReportedDataFrom(Packet packet) { + // Check if the packet includes the DataForm extension + PacketExtension packetExtension = packet.getExtension("x","jabber:x:data"); + if (packetExtension != null) { + // Check if the existing DataForm is a result of a search + DataForm dataForm = (DataForm) packetExtension; + if (dataForm.getReportedData() != null) + return new ReportedData(dataForm); + } + // Otherwise return null + return null; + } + + + /** + * Creates a new ReportedData based on the returned dataForm from a search + *(namespace "jabber:iq:search"). + * + * @param dataForm the dataForm returned from a search (namespace "jabber:iq:search"). + */ + private ReportedData(DataForm dataForm) { + // Add the columns to the report based on the reported data fields + for (Iterator fields = dataForm.getReportedData().getFields(); fields.hasNext();) { + FormField field = (FormField)fields.next(); + columns.add(new Column(field.getLabel(), field.getVariable(), field.getType())); + } + + // Add the rows to the report based on the form's items + for (Iterator items = dataForm.getItems(); items.hasNext();) { + DataForm.Item item = (DataForm.Item)items.next(); + List<Field> fieldList = new ArrayList<Field>(columns.size()); + FormField field; + for (Iterator fields = item.getFields(); fields.hasNext();) { + field = (FormField) fields.next(); + // The field is created with all the values of the data form's field + List<String> values = new ArrayList<String>(); + for (Iterator<String> it=field.getValues(); it.hasNext();) { + values.add(it.next()); + } + fieldList.add(new Field(field.getVariable(), values)); + } + rows.add(new Row(fieldList)); + } + + // Set the report's title + this.title = dataForm.getTitle(); + } + + + public ReportedData(){ + // Allow for model creation of ReportedData. + } + + /** + * Adds a new <code>Row</code>. + * @param row the new row to add. + */ + public void addRow(Row row){ + rows.add(row); + } + + /** + * Adds a new <code>Column</code> + * @param column the column to add. + */ + public void addColumn(Column column){ + columns.add(column); + } + + + /** + * Returns an Iterator for the rows returned from a search. + * + * @return an Iterator for the rows returned from a search. + */ + public Iterator<Row> getRows() { + return Collections.unmodifiableList(new ArrayList<Row>(rows)).iterator(); + } + + /** + * Returns an Iterator for the columns returned from a search. + * + * @return an Iterator for the columns returned from a search. + */ + public Iterator<Column> getColumns() { + return Collections.unmodifiableList(new ArrayList<Column>(columns)).iterator(); + } + + + /** + * Returns the report's title. It is similar to the title on a web page or an X + * window. + * + * @return title of the report. + */ + public String getTitle() { + return title; + } + + /** + * + * Represents the columns definition of the reported data. + * + * @author Gaston Dombiak + */ + public static class Column { + private String label; + private String variable; + private String type; + + /** + * Creates a new column with the specified definition. + * + * @param label the columns's label. + * @param variable the variable name of the column. + * @param type the format for the returned data. + */ + public Column(String label, String variable, String type) { + this.label = label; + this.variable = variable; + this.type = type; + } + + /** + * Returns the column's label. + * + * @return label of the column. + */ + public String getLabel() { + return label; + } + + + /** + * Returns the column's data format. Valid formats are: + * + * <ul> + * <li>text-single -> single line or word of text + * <li>text-private -> instead of showing the user what they typed, you show ***** to + * protect it + * <li>text-multi -> multiple lines of text entry + * <li>list-single -> given a list of choices, pick one + * <li>list-multi -> given a list of choices, pick one or more + * <li>boolean -> 0 or 1, true or false, yes or no. Default value is 0 + * <li>fixed -> fixed for putting in text to show sections, or just advertise your web + * site in the middle of the form + * <li>hidden -> is not given to the user at all, but returned with the questionnaire + * <li>jid-single -> Jabber ID - choosing a JID from your roster, and entering one based + * on the rules for a JID. + * <li>jid-multi -> multiple entries for JIDs + * </ul> + * + * @return format for the returned data. + */ + public String getType() { + return type; + } + + + /** + * Returns the variable name that the column is showing. + * + * @return the variable name of the column. + */ + public String getVariable() { + return variable; + } + + + } + + public static class Row { + private List<Field> fields = new ArrayList<Field>(); + + public Row(List<Field> fields) { + this.fields = fields; + } + + /** + * Returns the values of the field whose variable matches the requested variable. + * + * @param variable the variable to match. + * @return the values of the field whose variable matches the requested variable. + */ + public Iterator getValues(String variable) { + for(Iterator<Field> it=getFields();it.hasNext();) { + Field field = it.next(); + if (variable.equalsIgnoreCase(field.getVariable())) { + return field.getValues(); + } + } + return null; + } + + /** + * Returns the fields that define the data that goes with the item. + * + * @return the fields that define the data that goes with the item. + */ + private Iterator<Field> getFields() { + return Collections.unmodifiableList(new ArrayList<Field>(fields)).iterator(); + } + } + + public static class Field { + private String variable; + private List<String> values; + + public Field(String variable, List<String> values) { + this.variable = variable; + this.values = values; + } + + /** + * Returns the variable name that the field represents. + * + * @return the variable name of the field. + */ + public String getVariable() { + return variable; + } + + /** + * Returns an iterator on the values reported as part of the search. + * + * @return the returned values of the search. + */ + public Iterator<String> getValues() { + return Collections.unmodifiableList(values).iterator(); + } + } +} diff --git a/src/org/jivesoftware/smackx/RosterExchangeListener.java b/src/org/jivesoftware/smackx/RosterExchangeListener.java new file mode 100644 index 0000000..16ce559 --- /dev/null +++ b/src/org/jivesoftware/smackx/RosterExchangeListener.java @@ -0,0 +1,42 @@ +/** + * $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.smackx; + +import java.util.Iterator; + +/** + * + * A listener that is fired anytime a roster exchange is received. + * + * @author Gaston Dombiak + */ +public interface RosterExchangeListener { + + /** + * Called when roster entries are received as part of a roster exchange. + * + * @param from the user that sent the entries. + * @param remoteRosterEntries the entries sent by the user. The entries are instances of + * RemoteRosterEntry. + */ + public void entriesReceived(String from, Iterator<RemoteRosterEntry> remoteRosterEntries); + +} diff --git a/src/org/jivesoftware/smackx/RosterExchangeManager.java b/src/org/jivesoftware/smackx/RosterExchangeManager.java new file mode 100644 index 0000000..c1d193b --- /dev/null +++ b/src/org/jivesoftware/smackx/RosterExchangeManager.java @@ -0,0 +1,187 @@ +/** + * $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.smackx; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.Roster; +import org.jivesoftware.smack.RosterEntry; +import org.jivesoftware.smack.RosterGroup; +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.filter.PacketExtensionFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.packet.RosterExchange; + +/** + * + * Manages Roster exchanges. A RosterExchangeManager provides a high level access to send + * rosters, roster groups and roster entries to XMPP clients. It also provides an easy way + * to hook up custom logic when entries are received from another XMPP client through + * RosterExchangeListeners. + * + * @author Gaston Dombiak + */ +public class RosterExchangeManager { + + private List<RosterExchangeListener> rosterExchangeListeners = new ArrayList<RosterExchangeListener>(); + + private Connection con; + + private PacketFilter packetFilter = new PacketExtensionFilter("x", "jabber:x:roster"); + private PacketListener packetListener; + + /** + * Creates a new roster exchange manager. + * + * @param con a Connection which is used to send and receive messages. + */ + public RosterExchangeManager(Connection con) { + this.con = con; + init(); + } + + /** + * Adds a listener to roster exchanges. The listener will be fired anytime roster entries + * are received from remote XMPP clients. + * + * @param rosterExchangeListener a roster exchange listener. + */ + public void addRosterListener(RosterExchangeListener rosterExchangeListener) { + synchronized (rosterExchangeListeners) { + if (!rosterExchangeListeners.contains(rosterExchangeListener)) { + rosterExchangeListeners.add(rosterExchangeListener); + } + } + } + + /** + * Removes a listener from roster exchanges. The listener will be fired anytime roster + * entries are received from remote XMPP clients. + * + * @param rosterExchangeListener a roster exchange listener.. + */ + public void removeRosterListener(RosterExchangeListener rosterExchangeListener) { + synchronized (rosterExchangeListeners) { + rosterExchangeListeners.remove(rosterExchangeListener); + } + } + + /** + * Sends a roster to userID. All the entries of the roster will be sent to the + * target user. + * + * @param roster the roster to send + * @param targetUserID the user that will receive the roster entries + */ + public void send(Roster roster, String targetUserID) { + // Create a new message to send the roster + Message msg = new Message(targetUserID); + // Create a RosterExchange Package and add it to the message + RosterExchange rosterExchange = new RosterExchange(roster); + msg.addExtension(rosterExchange); + + // Send the message that contains the roster + con.sendPacket(msg); + } + + /** + * Sends a roster entry to userID. + * + * @param rosterEntry the roster entry to send + * @param targetUserID the user that will receive the roster entries + */ + public void send(RosterEntry rosterEntry, String targetUserID) { + // Create a new message to send the roster + Message msg = new Message(targetUserID); + // Create a RosterExchange Package and add it to the message + RosterExchange rosterExchange = new RosterExchange(); + rosterExchange.addRosterEntry(rosterEntry); + msg.addExtension(rosterExchange); + + // Send the message that contains the roster + con.sendPacket(msg); + } + + /** + * Sends a roster group to userID. All the entries of the group will be sent to the + * target user. + * + * @param rosterGroup the roster group to send + * @param targetUserID the user that will receive the roster entries + */ + public void send(RosterGroup rosterGroup, String targetUserID) { + // Create a new message to send the roster + Message msg = new Message(targetUserID); + // Create a RosterExchange Package and add it to the message + RosterExchange rosterExchange = new RosterExchange(); + for (RosterEntry entry : rosterGroup.getEntries()) { + rosterExchange.addRosterEntry(entry); + } + msg.addExtension(rosterExchange); + + // Send the message that contains the roster + con.sendPacket(msg); + } + + /** + * Fires roster exchange listeners. + */ + private void fireRosterExchangeListeners(String from, Iterator<RemoteRosterEntry> remoteRosterEntries) { + RosterExchangeListener[] listeners = null; + synchronized (rosterExchangeListeners) { + listeners = new RosterExchangeListener[rosterExchangeListeners.size()]; + rosterExchangeListeners.toArray(listeners); + } + for (int i = 0; i < listeners.length; i++) { + listeners[i].entriesReceived(from, remoteRosterEntries); + } + } + + private void init() { + // Listens for all roster exchange packets and fire the roster exchange listeners. + packetListener = new PacketListener() { + public void processPacket(Packet packet) { + Message message = (Message) packet; + RosterExchange rosterExchange = + (RosterExchange) message.getExtension("x", "jabber:x:roster"); + // Fire event for roster exchange listeners + fireRosterExchangeListeners(message.getFrom(), rosterExchange.getRosterEntries()); + }; + + }; + con.addPacketListener(packetListener, packetFilter); + } + + public void destroy() { + if (con != null) + con.removePacketListener(packetListener); + + } + protected void finalize() throws Throwable { + destroy(); + super.finalize(); + } +} diff --git a/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java b/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java new file mode 100644 index 0000000..9e31f67 --- /dev/null +++ b/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java @@ -0,0 +1,708 @@ +/** + * $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.smackx; + +import 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.PacketExtension; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smackx.entitycaps.EntityCapsManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverInfo.Identity; +import org.jivesoftware.smackx.packet.DiscoverItems; +import org.jivesoftware.smackx.packet.DataForm; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Manages discovery of services in XMPP entities. This class provides: + * <ol> + * <li>A registry of supported features in this XMPP entity. + * <li>Automatic response when this XMPP entity is queried for information. + * <li>Ability to discover items and information of remote XMPP entities. + * <li>Ability to publish publicly available items. + * </ol> + * + * @author Gaston Dombiak + */ +public class ServiceDiscoveryManager { + + private static final String DEFAULT_IDENTITY_NAME = "Smack"; + private static final String DEFAULT_IDENTITY_CATEGORY = "client"; + private static final String DEFAULT_IDENTITY_TYPE = "pc"; + + private static List<DiscoverInfo.Identity> identities = new LinkedList<DiscoverInfo.Identity>(); + + private EntityCapsManager capsManager; + + private static Map<Connection, ServiceDiscoveryManager> instances = + new ConcurrentHashMap<Connection, ServiceDiscoveryManager>(); + + private Connection connection; + private final Set<String> features = new HashSet<String>(); + private DataForm extendedInfo = null; + private Map<String, NodeInformationProvider> nodeInformationProviders = + new ConcurrentHashMap<String, NodeInformationProvider>(); + + // Create a new ServiceDiscoveryManager on every established connection + static { + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(Connection connection) { + new ServiceDiscoveryManager(connection); + } + }); + identities.add(new Identity(DEFAULT_IDENTITY_CATEGORY, DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE)); + } + + /** + * Creates a new ServiceDiscoveryManager for a given Connection. This means that the + * service manager will respond to any service discovery request that the connection may + * receive. + * + * @param connection the connection to which a ServiceDiscoveryManager is going to be created. + */ + public ServiceDiscoveryManager(Connection connection) { + this.connection = connection; + + init(); + } + + /** + * Returns the ServiceDiscoveryManager instance associated with a given Connection. + * + * @param connection the connection used to look for the proper ServiceDiscoveryManager. + * @return the ServiceDiscoveryManager associated with a given Connection. + */ + public static ServiceDiscoveryManager getInstanceFor(Connection connection) { + return instances.get(connection); + } + + /** + * Returns the name of the client that will be returned when asked for the client identity + * in a disco request. The name could be any value you need to identity this client. + * + * @return the name of the client that will be returned when asked for the client identity + * in a disco request. + */ + public static String getIdentityName() { + DiscoverInfo.Identity identity = identities.get(0); + if (identity != null) { + return identity.getName(); + } else { + return null; + } + } + + /** + * Sets the name of the client that will be returned when asked for the client identity + * in a disco request. The name could be any value you need to identity this client. + * + * @param name the name of the client that will be returned when asked for the client identity + * in a disco request. + */ + public static void setIdentityName(String name) { + DiscoverInfo.Identity identity = identities.remove(0); + identity = new DiscoverInfo.Identity(DEFAULT_IDENTITY_CATEGORY, name, DEFAULT_IDENTITY_TYPE); + identities.add(identity); + } + + /** + * Returns the type of client that will be returned when asked for the client identity in a + * disco request. The valid types are defined by the category client. Follow this link to learn + * the possible types: <a href="http://xmpp.org/registrar/disco-categories.html#client">Jabber::Registrar</a>. + * + * @return the type of client that will be returned when asked for the client identity in a + * disco request. + */ + public static String getIdentityType() { + DiscoverInfo.Identity identity = identities.get(0); + if (identity != null) { + return identity.getType(); + } else { + return null; + } + } + + /** + * Sets the type of client that will be returned when asked for the client identity in a + * disco request. The valid types are defined by the category client. Follow this link to learn + * the possible types: <a href="http://xmpp.org/registrar/disco-categories.html#client">Jabber::Registrar</a>. + * + * @param type the type of client that will be returned when asked for the client identity in a + * disco request. + */ + public static void setIdentityType(String type) { + DiscoverInfo.Identity identity = identities.get(0); + if (identity != null) { + identity.setType(type); + } else { + identity = new DiscoverInfo.Identity(DEFAULT_IDENTITY_CATEGORY, DEFAULT_IDENTITY_NAME, type); + identities.add(identity); + } + } + + /** + * Returns all identities of this client as unmodifiable Collection + * + * @return + */ + public static List<DiscoverInfo.Identity> getIdentities() { + return Collections.unmodifiableList(identities); + } + + /** + * Initializes the packet listeners of the connection that will answer to any + * service discovery request. + */ + private void init() { + // Register the new instance and associate it with the connection + instances.put(connection, this); + + addFeature(DiscoverInfo.NAMESPACE); + addFeature(DiscoverItems.NAMESPACE); + + // 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 + } + }); + + // Listen for disco#items requests and answer with an empty result + PacketFilter packetFilter = new PacketTypeFilter(DiscoverItems.class); + PacketListener packetListener = new PacketListener() { + public void processPacket(Packet packet) { + DiscoverItems discoverItems = (DiscoverItems) packet; + // Send back the items defined in the client if the request is of type GET + if (discoverItems != null && discoverItems.getType() == IQ.Type.GET) { + DiscoverItems response = new DiscoverItems(); + response.setType(IQ.Type.RESULT); + response.setTo(discoverItems.getFrom()); + response.setPacketID(discoverItems.getPacketID()); + response.setNode(discoverItems.getNode()); + + // Add the defined items related to the requested node. Look for + // the NodeInformationProvider associated with the requested node. + NodeInformationProvider nodeInformationProvider = + getNodeInformationProvider(discoverItems.getNode()); + if (nodeInformationProvider != null) { + // Specified node was found, add node items + response.addItems(nodeInformationProvider.getNodeItems()); + // Add packet extensions + response.addExtensions(nodeInformationProvider.getNodePacketExtensions()); + } else if(discoverItems.getNode() != null) { + // Return <item-not-found/> error since client doesn't contain + // the specified node + response.setType(IQ.Type.ERROR); + response.setError(new XMPPError(XMPPError.Condition.item_not_found)); + } + connection.sendPacket(response); + } + } + }; + connection.addPacketListener(packetListener, packetFilter); + + // Listen for disco#info requests and answer the client's supported features + // To add a new feature as supported use the #addFeature message + packetFilter = new PacketTypeFilter(DiscoverInfo.class); + packetListener = new PacketListener() { + public void processPacket(Packet packet) { + DiscoverInfo discoverInfo = (DiscoverInfo) packet; + // Answer the client's supported features if the request is of the GET type + if (discoverInfo != null && discoverInfo.getType() == IQ.Type.GET) { + DiscoverInfo response = new DiscoverInfo(); + response.setType(IQ.Type.RESULT); + response.setTo(discoverInfo.getFrom()); + response.setPacketID(discoverInfo.getPacketID()); + response.setNode(discoverInfo.getNode()); + // Add the client's identity and features only if "node" is null + // and if the request was not send to a node. If Entity Caps are + // enabled the client's identity and features are may also added + // if the right node is chosen + if (discoverInfo.getNode() == null) { + addDiscoverInfoTo(response); + } + else { + // Disco#info was sent to a node. Check if we have information of the + // specified node + NodeInformationProvider nodeInformationProvider = + getNodeInformationProvider(discoverInfo.getNode()); + if (nodeInformationProvider != null) { + // Node was found. Add node features + response.addFeatures(nodeInformationProvider.getNodeFeatures()); + // Add node identities + response.addIdentities(nodeInformationProvider.getNodeIdentities()); + // Add packet extensions + response.addExtensions(nodeInformationProvider.getNodePacketExtensions()); + } + else { + // Return <item-not-found/> error since specified node was not found + response.setType(IQ.Type.ERROR); + response.setError(new XMPPError(XMPPError.Condition.item_not_found)); + } + } + connection.sendPacket(response); + } + } + }; + connection.addPacketListener(packetListener, packetFilter); + } + + /** + * Add discover info response data. + * + * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol; Example 2</a> + * + * @param response the discover info response packet + */ + public void addDiscoverInfoTo(DiscoverInfo response) { + // First add the identities of the connection + response.addIdentities(identities); + + // Add the registered features to the response + synchronized (features) { + for (Iterator<String> it = getFeatures(); it.hasNext();) { + response.addFeature(it.next()); + } + response.addExtension(extendedInfo); + } + } + + /** + * Returns the NodeInformationProvider responsible for providing information + * (ie items) related to a given node or <tt>null</null> if none.<p> + * + * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the + * NodeInformationProvider will provide information about the rooms where the user has joined. + * + * @param node the node that contains items associated with an entity not addressable as a JID. + * @return the NodeInformationProvider responsible for providing information related + * to a given node. + */ + private NodeInformationProvider getNodeInformationProvider(String node) { + if (node == null) { + return null; + } + return nodeInformationProviders.get(node); + } + + /** + * Sets the NodeInformationProvider responsible for providing information + * (ie items) related to a given node. Every time this client receives a disco request + * regarding the items of a given node, the provider associated to that node will be the + * responsible for providing the requested information.<p> + * + * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the + * NodeInformationProvider will provide information about the rooms where the user has joined. + * + * @param node the node whose items will be provided by the NodeInformationProvider. + * @param listener the NodeInformationProvider responsible for providing items related + * to the node. + */ + public void setNodeInformationProvider(String node, NodeInformationProvider listener) { + nodeInformationProviders.put(node, listener); + } + + /** + * Removes the NodeInformationProvider responsible for providing information + * (ie items) related to a given node. This means that no more information will be + * available for the specified node. + * + * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the + * NodeInformationProvider will provide information about the rooms where the user has joined. + * + * @param node the node to remove the associated NodeInformationProvider. + */ + public void removeNodeInformationProvider(String node) { + nodeInformationProviders.remove(node); + } + + /** + * Returns the supported features by this XMPP entity. + * + * @return an Iterator on the supported features by this XMPP entity. + */ + public Iterator<String> getFeatures() { + synchronized (features) { + return Collections.unmodifiableList(new ArrayList<String>(features)).iterator(); + } + } + + /** + * Returns the supported features by this XMPP entity. + * + * @return a copy of the List on the supported features by this XMPP entity. + */ + public List<String> getFeaturesList() { + synchronized (features) { + return new LinkedList<String>(features); + } + } + + /** + * Registers that a new feature is supported by this XMPP entity. When this client is + * queried for its information the registered features will be answered.<p> + * + * Since no packet is actually sent to the server it is safe to perform this operation + * before logging to the server. In fact, you may want to configure the supported features + * before logging to the server so that the information is already available if it is required + * upon login. + * + * @param feature the feature to register as supported. + */ + public void addFeature(String feature) { + synchronized (features) { + features.add(feature); + renewEntityCapsVersion(); + } + } + + /** + * Removes the specified feature from the supported features by this XMPP entity.<p> + * + * Since no packet is actually sent to the server it is safe to perform this operation + * before logging to the server. + * + * @param feature the feature to remove from the supported features. + */ + public void removeFeature(String feature) { + synchronized (features) { + features.remove(feature); + renewEntityCapsVersion(); + } + } + + /** + * Returns true if the specified feature is registered in the ServiceDiscoveryManager. + * + * @param feature the feature to look for. + * @return a boolean indicating if the specified featured is registered or not. + */ + public boolean includesFeature(String feature) { + synchronized (features) { + return features.contains(feature); + } + } + + /** + * Registers extended discovery information of this XMPP entity. When this + * client is queried for its information this data form will be returned as + * specified by XEP-0128. + * <p> + * + * Since no packet is actually sent to the server it is safe to perform this + * operation before logging to the server. In fact, you may want to + * configure the extended info before logging to the server so that the + * information is already available if it is required upon login. + * + * @param info + * the data form that contains the extend service discovery + * information. + */ + public void setExtendedInfo(DataForm info) { + extendedInfo = info; + renewEntityCapsVersion(); + } + + /** + * Returns the data form that is set as extended information for this Service Discovery instance (XEP-0128) + * + * @see <a href="http://xmpp.org/extensions/xep-0128.html">XEP-128: Service Discovery Extensions</a> + * @return + */ + public DataForm getExtendedInfo() { + return extendedInfo; + } + + /** + * Returns the data form as List of PacketExtensions, or null if no data form is set. + * This representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider) + * + * @return + */ + public List<PacketExtension> getExtendedInfoAsList() { + List<PacketExtension> res = null; + if (extendedInfo != null) { + res = new ArrayList<PacketExtension>(1); + res.add(extendedInfo); + } + return res; + } + + /** + * Removes the data form containing extended service discovery information + * from the information returned by this XMPP entity.<p> + * + * Since no packet is actually sent to the server it is safe to perform this + * operation before logging to the server. + */ + public void removeExtendedInfo() { + extendedInfo = null; + renewEntityCapsVersion(); + } + + /** + * Returns the discovered information of a given XMPP entity addressed by its JID. + * Use null as entityID to query the server + * + * @param entityID the address of the XMPP entity or null. + * @return the discovered information. + * @throws XMPPException if the operation failed for some reason. + */ + public DiscoverInfo discoverInfo(String entityID) throws XMPPException { + if (entityID == null) + return discoverInfo(null, null); + + // Check if the have it cached in the Entity Capabilities Manager + DiscoverInfo info = EntityCapsManager.getDiscoverInfoByUser(entityID); + + if (info != null) { + // We were able to retrieve the information from Entity Caps and + // avoided a disco request, hurray! + return info; + } + + // Try to get the newest node#version if it's known, otherwise null is + // returned + EntityCapsManager.NodeVerHash nvh = EntityCapsManager.getNodeVerHashByJid(entityID); + + // Discover by requesting the information from the remote entity + // Note that wee need to use NodeVer as argument for Node if it exists + info = discoverInfo(entityID, nvh != null ? nvh.getNodeVer() : null); + + // If the node version is known, store the new entry. + if (nvh != null) { + if (EntityCapsManager.verifyDiscoverInfoVersion(nvh.getVer(), nvh.getHash(), info)) + EntityCapsManager.addDiscoverInfoByNode(nvh.getNodeVer(), info); + } + + return info; + } + + /** + * Returns the discovered information of a given XMPP entity addressed by its JID and + * note attribute. Use this message only when trying to query information which is not + * directly addressable. + * + * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol</a> + * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-nodes">XEP-30 Info Nodes</a> + * + * @param entityID the address of the XMPP entity. + * @param node the optional attribute that supplements the 'jid' attribute. + * @return the discovered information. + * @throws XMPPException if the operation failed for some reason. + */ + public DiscoverInfo discoverInfo(String entityID, String node) throws XMPPException { + // Discover the entity's info + DiscoverInfo disco = new DiscoverInfo(); + disco.setType(IQ.Type.GET); + disco.setTo(entityID); + disco.setNode(node); + + // Create a packet collector to listen for a response. + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(disco.getPacketID())); + + connection.sendPacket(disco); + + // Wait up to 5 seconds for a result. + IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from the server."); + } + if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + return (DiscoverInfo) result; + } + + /** + * Returns the discovered items of a given XMPP entity addressed by its JID. + * + * @param entityID the address of the XMPP entity. + * @return the discovered information. + * @throws XMPPException if the operation failed for some reason. + */ + public DiscoverItems discoverItems(String entityID) throws XMPPException { + return discoverItems(entityID, null); + } + + /** + * Returns the discovered items of a given XMPP entity addressed by its JID and + * note attribute. Use this message only when trying to query information which is not + * directly addressable. + * + * @param entityID the address of the XMPP entity. + * @param node the optional attribute that supplements the 'jid' attribute. + * @return the discovered items. + * @throws XMPPException if the operation failed for some reason. + */ + public DiscoverItems discoverItems(String entityID, String node) throws XMPPException { + // Discover the entity's items + DiscoverItems disco = new DiscoverItems(); + disco.setType(IQ.Type.GET); + disco.setTo(entityID); + disco.setNode(node); + + // Create a packet collector to listen for a response. + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(disco.getPacketID())); + + connection.sendPacket(disco); + + // Wait up to 5 seconds for a result. + IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from the server."); + } + if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + return (DiscoverItems) result; + } + + /** + * Returns true if the server supports publishing of items. A client may wish to publish items + * to the server so that the server can provide items associated to the client. These items will + * be returned by the server whenever the server receives a disco request targeted to the bare + * address of the client (i.e. user@host.com). + * + * @param entityID the address of the XMPP entity. + * @return true if the server supports publishing of items. + * @throws XMPPException if the operation failed for some reason. + */ + public boolean canPublishItems(String entityID) throws XMPPException { + DiscoverInfo info = discoverInfo(entityID); + return canPublishItems(info); + } + + /** + * Returns true if the server supports publishing of items. A client may wish to publish items + * to the server so that the server can provide items associated to the client. These items will + * be returned by the server whenever the server receives a disco request targeted to the bare + * address of the client (i.e. user@host.com). + * + * @param DiscoverInfo the discover info packet to check. + * @return true if the server supports publishing of items. + */ + public static boolean canPublishItems(DiscoverInfo info) { + return info.containsFeature("http://jabber.org/protocol/disco#publish"); + } + + /** + * Publishes new items to a parent entity. The item elements to publish MUST have at least + * a 'jid' attribute specifying the Entity ID of the item, and an action attribute which + * specifies the action being taken for that item. Possible action values are: "update" and + * "remove". + * + * @param entityID the address of the XMPP entity. + * @param discoverItems the DiscoveryItems to publish. + * @throws XMPPException if the operation failed for some reason. + */ + public void publishItems(String entityID, DiscoverItems discoverItems) + throws XMPPException { + publishItems(entityID, null, discoverItems); + } + + /** + * Publishes new items to a parent entity and node. The item elements to publish MUST have at + * least a 'jid' attribute specifying the Entity ID of the item, and an action attribute which + * specifies the action being taken for that item. Possible action values are: "update" and + * "remove". + * + * @param entityID the address of the XMPP entity. + * @param node the attribute that supplements the 'jid' attribute. + * @param discoverItems the DiscoveryItems to publish. + * @throws XMPPException if the operation failed for some reason. + */ + public void publishItems(String entityID, String node, DiscoverItems discoverItems) + throws XMPPException { + discoverItems.setType(IQ.Type.SET); + discoverItems.setTo(entityID); + discoverItems.setNode(node); + + // Create a packet collector to listen for a response. + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(discoverItems.getPacketID())); + + connection.sendPacket(discoverItems); + + // Wait up to 5 seconds for a result. + IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from the server."); + } + if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + } + + /** + * Entity Capabilities + */ + + /** + * Loads the ServiceDiscoveryManager with an EntityCapsManger + * that speeds up certain lookups + * @param manager + */ + public void setEntityCapsManager(EntityCapsManager manager) { + capsManager = manager; + } + + /** + * Updates the Entity Capabilities Verification String + * if EntityCaps is enabled + */ + private void renewEntityCapsVersion() { + if (capsManager != null && capsManager.entityCapsEnabled()) + capsManager.updateLocalEntityCaps(); + } +} diff --git a/src/org/jivesoftware/smackx/SharedGroupManager.java b/src/org/jivesoftware/smackx/SharedGroupManager.java new file mode 100644 index 0000000..76cd527 --- /dev/null +++ b/src/org/jivesoftware/smackx/SharedGroupManager.java @@ -0,0 +1,72 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 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.smackx;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.packet.SharedGroupsInfo;
+
+import java.util.List;
+
+/**
+ * A SharedGroupManager provides services for discovering the shared groups where a user belongs.<p>
+ *
+ * Important note: This functionality is not part of the XMPP spec and it will only work
+ * with Wildfire.
+ *
+ * @author Gaston Dombiak
+ */
+public class SharedGroupManager {
+
+ /**
+ * Returns the collection that will contain the name of the shared groups where the user
+ * logged in with the specified session belongs.
+ *
+ * @param connection connection to use to get the user's shared groups.
+ * @return collection with the shared groups' name of the logged user.
+ */
+ public static List<String> getSharedGroups(Connection connection) throws XMPPException {
+ // Discover the shared groups of the logged user
+ SharedGroupsInfo info = new SharedGroupsInfo();
+ info.setType(IQ.Type.GET);
+
+ // Create a packet collector to listen for a response.
+ PacketCollector collector =
+ connection.createPacketCollector(new PacketIDFilter(info.getPacketID()));
+
+ connection.sendPacket(info);
+
+ // Wait up to 5 seconds for a result.
+ IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ return ((SharedGroupsInfo) result).getGroups();
+ }
+}
diff --git a/src/org/jivesoftware/smackx/XHTMLManager.java b/src/org/jivesoftware/smackx/XHTMLManager.java new file mode 100644 index 0000000..a446819 --- /dev/null +++ b/src/org/jivesoftware/smackx/XHTMLManager.java @@ -0,0 +1,144 @@ +/** + * $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.smackx; + +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.XHTMLExtension; + +import java.util.Iterator; + +/** + * Manages XHTML formatted texts within messages. A XHTMLManager provides a high level access to + * get and set XHTML bodies to messages, enable and disable XHTML support and check if remote XMPP + * clients support XHTML. + * + * @author Gaston Dombiak + */ +public class XHTMLManager { + + private final static String namespace = "http://jabber.org/protocol/xhtml-im"; + + // Enable the XHTML support on every established connection + // The ServiceDiscoveryManager class should have been already initialized + static { + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(Connection connection) { + XHTMLManager.setServiceEnabled(connection, true); + } + }); + } + + /** + * Returns an Iterator for the XHTML bodies in the message. Returns null if + * the message does not contain an XHTML extension. + * + * @param message an XHTML message + * @return an Iterator for the bodies in the message or null if none. + */ + public static Iterator<String> getBodies(Message message) { + XHTMLExtension xhtmlExtension = (XHTMLExtension) message.getExtension("html", namespace); + if (xhtmlExtension != null) + return xhtmlExtension.getBodies(); + else + return null; + } + + /** + * Adds an XHTML body to the message. + * + * @param message the message that will receive the XHTML body + * @param body the string to add as an XHTML body to the message + */ + public static void addBody(Message message, String body) { + XHTMLExtension xhtmlExtension = (XHTMLExtension) message.getExtension("html", namespace); + if (xhtmlExtension == null) { + // Create an XHTMLExtension and add it to the message + xhtmlExtension = new XHTMLExtension(); + message.addExtension(xhtmlExtension); + } + // Add the required bodies to the message + xhtmlExtension.addBody(body); + } + + /** + * Returns true if the message contains an XHTML extension. + * + * @param message the message to check if contains an XHTML extentsion or not + * @return a boolean indicating whether the message is an XHTML message + */ + public static boolean isXHTMLMessage(Message message) { + return message.getExtension("html", namespace) != null; + } + + /** + * Enables or disables the XHTML support on a given connection.<p> + * + * Before starting to send XHTML messages to a user, check that the user can handle XHTML + * messages. Enable the XHTML support to indicate that this client handles XHTML messages. + * + * @param connection the connection where the service will be enabled or disabled + * @param enabled indicates if the service will be enabled or disabled + */ + public synchronized static void setServiceEnabled(Connection connection, boolean enabled) { + if (isServiceEnabled(connection) == enabled) + return; + + if (enabled) { + ServiceDiscoveryManager.getInstanceFor(connection).addFeature(namespace); + } + else { + ServiceDiscoveryManager.getInstanceFor(connection).removeFeature(namespace); + } + } + + /** + * Returns true if the XHTML support is enabled for the given connection. + * + * @param connection the connection to look for XHTML support + * @return a boolean indicating if the XHTML support is enabled for the given connection + */ + public static boolean isServiceEnabled(Connection connection) { + return ServiceDiscoveryManager.getInstanceFor(connection).includesFeature(namespace); + } + + /** + * Returns true if the specified user handles XHTML messages. + * + * @param connection the connection to use to perform the service discovery + * @param userID the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com + * @return a boolean indicating whether the specified user handles XHTML messages + */ + public static boolean isServiceEnabled(Connection connection, String userID) { + try { + DiscoverInfo result = + ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(userID); + return result.containsFeature(namespace); + } + catch (XMPPException e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/src/org/jivesoftware/smackx/XHTMLText.java b/src/org/jivesoftware/smackx/XHTMLText.java new file mode 100644 index 0000000..201e530 --- /dev/null +++ b/src/org/jivesoftware/smackx/XHTMLText.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.smackx; + +import org.jivesoftware.smack.util.StringUtils; + +/** + * An XHTMLText represents formatted text. This class also helps to build valid + * XHTML tags. + * + * @author Gaston Dombiak + */ +public class XHTMLText { + + private StringBuilder text = new StringBuilder(30); + + /** + * Creates a new XHTMLText with body tag params. + * + * @param style the XHTML style of the body + * @param lang the language of the body + */ + public XHTMLText(String style, String lang) { + appendOpenBodyTag(style, lang); + } + + /** + * Appends a tag that indicates that an anchor section begins. + * + * @param href indicates the URL being linked to + * @param style the XHTML style of the anchor + */ + public void appendOpenAnchorTag(String href, String style) { + StringBuilder sb = new StringBuilder("<a"); + if (href != null) { + sb.append(" href=\""); + sb.append(href); + sb.append("\""); + } + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that an anchor section ends. + * + */ + public void appendCloseAnchorTag() { + text.append("</a>"); + } + + /** + * Appends a tag that indicates that a blockquote section begins. + * + * @param style the XHTML style of the blockquote + */ + public void appendOpenBlockQuoteTag(String style) { + StringBuilder sb = new StringBuilder("<blockquote"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that a blockquote section ends. + * + */ + public void appendCloseBlockQuoteTag() { + text.append("</blockquote>"); + } + + /** + * Appends a tag that indicates that a body section begins. + * + * @param style the XHTML style of the body + * @param lang the language of the body + */ + private void appendOpenBodyTag(String style, String lang) { + StringBuilder sb = new StringBuilder("<body"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + if (lang != null) { + sb.append(" xml:lang=\""); + sb.append(lang); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that a body section ends. + * + */ + private String closeBodyTag() { + return "</body>"; + } + + /** + * Appends a tag that inserts a single carriage return. + * + */ + public void appendBrTag() { + text.append("<br/>"); + } + + /** + * Appends a tag that indicates a reference to work, such as a book, report or web site. + * + */ + public void appendOpenCiteTag() { + text.append("<cite>"); + } + + /** + * Appends a tag that indicates text that is the code for a program. + * + */ + public void appendOpenCodeTag() { + text.append("<code>"); + } + + /** + * Appends a tag that indicates end of text that is the code for a program. + * + */ + public void appendCloseCodeTag() { + text.append("</code>"); + } + + /** + * Appends a tag that indicates emphasis. + * + */ + public void appendOpenEmTag() { + text.append("<em>"); + } + + /** + * Appends a tag that indicates end of emphasis. + * + */ + public void appendCloseEmTag() { + text.append("</em>"); + } + + /** + * Appends a tag that indicates a header, a title of a section of the message. + * + * @param level the level of the Header. It should be a value between 1 and 3 + * @param style the XHTML style of the blockquote + */ + public void appendOpenHeaderTag(int level, String style) { + if (level > 3 || level < 1) { + return; + } + StringBuilder sb = new StringBuilder("<h"); + sb.append(level); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that a header section ends. + * + * @param level the level of the Header. It should be a value between 1 and 3 + */ + public void appendCloseHeaderTag(int level) { + if (level > 3 || level < 1) { + return; + } + StringBuilder sb = new StringBuilder("</h"); + sb.append(level); + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates an image. + * + * @param align how text should flow around the picture + * @param alt the text to show if you don't show the picture + * @param height how tall is the picture + * @param src where to get the picture + * @param width how wide is the picture + */ + public void appendImageTag(String align, String alt, String height, String src, String width) { + StringBuilder sb = new StringBuilder("<img"); + if (align != null) { + sb.append(" align=\""); + sb.append(align); + sb.append("\""); + } + if (alt != null) { + sb.append(" alt=\""); + sb.append(alt); + sb.append("\""); + } + if (height != null) { + sb.append(" height=\""); + sb.append(height); + sb.append("\""); + } + if (src != null) { + sb.append(" src=\""); + sb.append(src); + sb.append("\""); + } + if (width != null) { + sb.append(" width=\""); + sb.append(width); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates the start of a new line item within a list. + * + * @param style the style of the line item + */ + public void appendLineItemTag(String style) { + StringBuilder sb = new StringBuilder("<li"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that creates an ordered list. "Ordered" means that the order of the items + * in the list is important. To show this, browsers automatically number the list. + * + * @param style the style of the ordered list + */ + public void appendOpenOrderedListTag(String style) { + StringBuilder sb = new StringBuilder("<ol"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that an ordered list section ends. + * + */ + public void appendCloseOrderedListTag() { + text.append("</ol>"); + } + + /** + * Appends a tag that creates an unordered list. The unordered part means that the items + * in the list are not in any particular order. + * + * @param style the style of the unordered list + */ + public void appendOpenUnorderedListTag(String style) { + StringBuilder sb = new StringBuilder("<ul"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that an unordered list section ends. + * + */ + public void appendCloseUnorderedListTag() { + text.append("</ul>"); + } + + /** + * Appends a tag that indicates the start of a new paragraph. This is usually rendered + * with two carriage returns, producing a single blank line in between the two paragraphs. + * + * @param style the style of the paragraph + */ + public void appendOpenParagraphTag(String style) { + StringBuilder sb = new StringBuilder("<p"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates the end of a new paragraph. This is usually rendered + * with two carriage returns, producing a single blank line in between the two paragraphs. + * + */ + public void appendCloseParagraphTag() { + text.append("</p>"); + } + + /** + * Appends a tag that indicates that an inlined quote section begins. + * + * @param style the style of the inlined quote + */ + public void appendOpenInlinedQuoteTag(String style) { + StringBuilder sb = new StringBuilder("<q"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that an inlined quote section ends. + * + */ + public void appendCloseInlinedQuoteTag() { + text.append("</q>"); + } + + /** + * Appends a tag that allows to set the fonts for a span of text. + * + * @param style the style for a span of text + */ + public void appendOpenSpanTag(String style) { + StringBuilder sb = new StringBuilder("<span"); + if (style != null) { + sb.append(" style=\""); + sb.append(style); + sb.append("\""); + } + sb.append(">"); + text.append(sb.toString()); + } + + /** + * Appends a tag that indicates that a span section ends. + * + */ + public void appendCloseSpanTag() { + text.append("</span>"); + } + + /** + * Appends a tag that indicates text which should be more forceful than surrounding text. + * + */ + public void appendOpenStrongTag() { + text.append("<strong>"); + } + + /** + * Appends a tag that indicates that a strong section ends. + * + */ + public void appendCloseStrongTag() { + text.append("</strong>"); + } + + /** + * Appends a given text to the XHTMLText. + * + * @param textToAppend the text to append + */ + public void append(String textToAppend) { + text.append(StringUtils.escapeForXML(textToAppend)); + } + + /** + * Returns the text of the XHTMLText. + * + * Note: Automatically adds the closing body tag. + * + * @return the text of the XHTMLText + */ + public String toString() { + return text.toString().concat(closeBodyTag()); + } + +} diff --git a/src/org/jivesoftware/smackx/bookmark/BookmarkManager.java b/src/org/jivesoftware/smackx/bookmark/BookmarkManager.java new file mode 100644 index 0000000..f85cc9c --- /dev/null +++ b/src/org/jivesoftware/smackx/bookmark/BookmarkManager.java @@ -0,0 +1,224 @@ +/** + * $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.smackx.bookmark; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.PrivateDataManager; + +import java.util.*; + +/** + * Provides methods to manage bookmarks in accordance with JEP-0048. Methods for managing URLs and + * Conferences are provided. + * </p> + * It should be noted that some extensions have been made to the JEP. There is an attribute on URLs + * that marks a url as a news feed and also a sub-element can be added to either a URL or conference + * indicated that it is shared amongst all users on a server. + * + * @author Alexander Wenckus + */ +public class BookmarkManager { + private static final Map<Connection, BookmarkManager> bookmarkManagerMap = new HashMap<Connection, BookmarkManager>(); + static { + PrivateDataManager.addPrivateDataProvider("storage", "storage:bookmarks", + new Bookmarks.Provider()); + } + + /** + * Returns the <i>BookmarkManager</i> for a connection, if it doesn't exist it is created. + * + * @param connection the connection for which the manager is desired. + * @return Returns the <i>BookmarkManager</i> for a connection, if it doesn't + * exist it is created. + * @throws XMPPException Thrown if the connection is null or has not yet been authenticated. + */ + public synchronized static BookmarkManager getBookmarkManager(Connection connection) + throws XMPPException + { + BookmarkManager manager = (BookmarkManager) bookmarkManagerMap.get(connection); + if(manager == null) { + manager = new BookmarkManager(connection); + bookmarkManagerMap.put(connection, manager); + } + return manager; + } + + private PrivateDataManager privateDataManager; + private Bookmarks bookmarks; + private final Object bookmarkLock = new Object(); + + /** + * Default constructor. Registers the data provider with the private data manager in the + * storage:bookmarks namespace. + * + * @param connection the connection for persisting and retrieving bookmarks. + * @throws XMPPException thrown when the connection is null or has not been authenticated. + */ + private BookmarkManager(Connection connection) throws XMPPException { + if(connection == null || !connection.isAuthenticated()) { + throw new XMPPException("Invalid connection."); + } + this.privateDataManager = new PrivateDataManager(connection); + } + + /** + * Returns all currently bookmarked conferences. + * + * @return returns all currently bookmarked conferences + * @throws XMPPException thrown when there was an error retrieving the current bookmarks from + * the server. + * @see BookmarkedConference + */ + public Collection<BookmarkedConference> getBookmarkedConferences() throws XMPPException { + retrieveBookmarks(); + return Collections.unmodifiableCollection(bookmarks.getBookmarkedConferences()); + } + + /** + * Adds or updates a conference in the bookmarks. + * + * @param name the name of the conference + * @param jid the jid of the conference + * @param isAutoJoin whether or not to join this conference automatically on login + * @param nickname the nickname to use for the user when joining the conference + * @param password the password to use for the user when joining the conference + * @throws XMPPException thrown when there is an issue retrieving the current bookmarks from + * the server. + */ + public void addBookmarkedConference(String name, String jid, boolean isAutoJoin, + String nickname, String password) throws XMPPException + { + retrieveBookmarks(); + BookmarkedConference bookmark + = new BookmarkedConference(name, jid, isAutoJoin, nickname, password); + List<BookmarkedConference> conferences = bookmarks.getBookmarkedConferences(); + if(conferences.contains(bookmark)) { + BookmarkedConference oldConference = conferences.get(conferences.indexOf(bookmark)); + if(oldConference.isShared()) { + throw new IllegalArgumentException("Cannot modify shared bookmark"); + } + oldConference.setAutoJoin(isAutoJoin); + oldConference.setName(name); + oldConference.setNickname(nickname); + oldConference.setPassword(password); + } + else { + bookmarks.addBookmarkedConference(bookmark); + } + privateDataManager.setPrivateData(bookmarks); + } + + /** + * Removes a conference from the bookmarks. + * + * @param jid the jid of the conference to be removed. + * @throws XMPPException thrown when there is a problem with the connection attempting to + * retrieve the bookmarks or persist the bookmarks. + * @throws IllegalArgumentException thrown when the conference being removed is a shared + * conference + */ + public void removeBookmarkedConference(String jid) throws XMPPException { + retrieveBookmarks(); + Iterator<BookmarkedConference> it = bookmarks.getBookmarkedConferences().iterator(); + while(it.hasNext()) { + BookmarkedConference conference = it.next(); + if(conference.getJid().equalsIgnoreCase(jid)) { + if(conference.isShared()) { + throw new IllegalArgumentException("Conference is shared and can't be removed"); + } + it.remove(); + privateDataManager.setPrivateData(bookmarks); + return; + } + } + } + + /** + * Returns an unmodifiable collection of all bookmarked urls. + * + * @return returns an unmodifiable collection of all bookmarked urls. + * @throws XMPPException thrown when there is a problem retriving bookmarks from the server. + */ + public Collection<BookmarkedURL> getBookmarkedURLs() throws XMPPException { + retrieveBookmarks(); + return Collections.unmodifiableCollection(bookmarks.getBookmarkedURLS()); + } + + /** + * Adds a new url or updates an already existing url in the bookmarks. + * + * @param URL the url of the bookmark + * @param name the name of the bookmark + * @param isRSS whether or not the url is an rss feed + * @throws XMPPException thrown when there is an error retriving or saving bookmarks from or to + * the server + */ + public void addBookmarkedURL(String URL, String name, boolean isRSS) throws XMPPException { + retrieveBookmarks(); + BookmarkedURL bookmark = new BookmarkedURL(URL, name, isRSS); + List<BookmarkedURL> urls = bookmarks.getBookmarkedURLS(); + if(urls.contains(bookmark)) { + BookmarkedURL oldURL = urls.get(urls.indexOf(bookmark)); + if(oldURL.isShared()) { + throw new IllegalArgumentException("Cannot modify shared bookmarks"); + } + oldURL.setName(name); + oldURL.setRss(isRSS); + } + else { + bookmarks.addBookmarkedURL(bookmark); + } + privateDataManager.setPrivateData(bookmarks); + } + + /** + * Removes a url from the bookmarks. + * + * @param bookmarkURL the url of the bookmark to remove + * @throws XMPPException thrown if there is an error retriving or saving bookmarks from or to + * the server. + */ + public void removeBookmarkedURL(String bookmarkURL) throws XMPPException { + retrieveBookmarks(); + Iterator<BookmarkedURL> it = bookmarks.getBookmarkedURLS().iterator(); + while(it.hasNext()) { + BookmarkedURL bookmark = it.next(); + if(bookmark.getURL().equalsIgnoreCase(bookmarkURL)) { + if(bookmark.isShared()) { + throw new IllegalArgumentException("Cannot delete a shared bookmark."); + } + it.remove(); + privateDataManager.setPrivateData(bookmarks); + return; + } + } + } + + private Bookmarks retrieveBookmarks() throws XMPPException { + synchronized(bookmarkLock) { + if(bookmarks == null) { + bookmarks = (Bookmarks) privateDataManager.getPrivateData("storage", + "storage:bookmarks"); + } + return bookmarks; + } + } +} diff --git a/src/org/jivesoftware/smackx/bookmark/BookmarkedConference.java b/src/org/jivesoftware/smackx/bookmark/BookmarkedConference.java new file mode 100644 index 0000000..5dac202 --- /dev/null +++ b/src/org/jivesoftware/smackx/bookmark/BookmarkedConference.java @@ -0,0 +1,130 @@ +/** + * $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.smackx.bookmark; + +/** + * Respresents a Conference Room bookmarked on the server using JEP-0048 Bookmark Storage JEP. + * + * @author Derek DeMoro + */ +public class BookmarkedConference implements SharedBookmark { + + private String name; + private boolean autoJoin; + private final String jid; + + private String nickname; + private String password; + private boolean isShared; + + protected BookmarkedConference(String jid) { + this.jid = jid; + } + + protected BookmarkedConference(String name, String jid, boolean autoJoin, String nickname, + String password) + { + this.name = name; + this.jid = jid; + this.autoJoin = autoJoin; + this.nickname = nickname; + this.password = password; + } + + + /** + * Returns the display label representing the Conference room. + * + * @return the name of the conference room. + */ + public String getName() { + return name; + } + + protected void setName(String name) { + this.name = name; + } + + /** + * Returns true if this conference room should be auto-joined on startup. + * + * @return true if room should be joined on startup, otherwise false. + */ + public boolean isAutoJoin() { + return autoJoin; + } + + protected void setAutoJoin(boolean autoJoin) { + this.autoJoin = autoJoin; + } + + /** + * Returns the full JID of this conference room. (ex.dev@conference.jivesoftware.com) + * + * @return the full JID of this conference room. + */ + public String getJid() { + return jid; + } + + /** + * Returns the nickname to use when joining this conference room. This is an optional + * value and may return null. + * + * @return the nickname to use when joining, null may be returned. + */ + public String getNickname() { + return nickname; + } + + protected void setNickname(String nickname) { + this.nickname = nickname; + } + + /** + * Returns the password to use when joining this conference room. This is an optional + * value and may return null. + * + * @return the password to use when joining this conference room, null may be returned. + */ + public String getPassword() { + return password; + } + + protected void setPassword(String password) { + this.password = password; + } + + public boolean equals(Object obj) { + if(obj == null || !(obj instanceof BookmarkedConference)) { + return false; + } + BookmarkedConference conference = (BookmarkedConference)obj; + return conference.getJid().equalsIgnoreCase(jid); + } + + protected void setShared(boolean isShared) { + this.isShared = isShared; + } + + public boolean isShared() { + return isShared; + } +} diff --git a/src/org/jivesoftware/smackx/bookmark/BookmarkedURL.java b/src/org/jivesoftware/smackx/bookmark/BookmarkedURL.java new file mode 100644 index 0000000..f3d6d9d --- /dev/null +++ b/src/org/jivesoftware/smackx/bookmark/BookmarkedURL.java @@ -0,0 +1,104 @@ +/** + * $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.smackx.bookmark; + +/** + * Respresents one instance of a URL defined using JEP-0048 Bookmark Storage JEP. + * + * @author Derek DeMoro + */ +public class BookmarkedURL implements SharedBookmark { + + private String name; + private final String URL; + private boolean isRss; + private boolean isShared; + + protected BookmarkedURL(String URL) { + this.URL = URL; + } + + protected BookmarkedURL(String URL, String name, boolean isRss) { + this.URL = URL; + this.name = name; + this.isRss = isRss; + } + + /** + * Returns the name representing the URL (eg. Jive Software). This can be used in as a label, or + * identifer in applications. + * + * @return the name reprenting the URL. + */ + public String getName() { + return name; + } + + /** + * Sets the name representing the URL. + * + * @param name the name. + */ + protected void setName(String name) { + this.name = name; + } + + /** + * Returns the URL. + * + * @return the url. + */ + public String getURL() { + return URL; + } + /** + * Set to true if this URL is an RSS or news feed. + * + * @param isRss True if the URL is a news feed and false if it is not. + */ + protected void setRss(boolean isRss) { + this.isRss = isRss; + } + + /** + * Returns true if this URL is a news feed. + * + * @return Returns true if this URL is a news feed. + */ + public boolean isRss() { + return isRss; + } + + public boolean equals(Object obj) { + if(!(obj instanceof BookmarkedURL)) { + return false; + } + BookmarkedURL url = (BookmarkedURL)obj; + return url.getURL().equalsIgnoreCase(URL); + } + + protected void setShared(boolean shared) { + this.isShared = shared; + } + + public boolean isShared() { + return isShared; + } +} diff --git a/src/org/jivesoftware/smackx/bookmark/Bookmarks.java b/src/org/jivesoftware/smackx/bookmark/Bookmarks.java new file mode 100644 index 0000000..100fa46 --- /dev/null +++ b/src/org/jivesoftware/smackx/bookmark/Bookmarks.java @@ -0,0 +1,310 @@ +/** + * $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.smackx.bookmark; + +import org.jivesoftware.smackx.packet.PrivateData; +import org.jivesoftware.smackx.provider.PrivateDataProvider; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Bookmarks is used for storing and retrieving URLS and Conference rooms. + * Bookmark Storage (JEP-0048) defined a protocol for the storage of bookmarks to conference rooms and other entities + * in a Jabber user's account. + * See the following code sample for saving Bookmarks: + * <p/> + * <pre> + * Connection con = new XMPPConnection("jabber.org"); + * con.login("john", "doe"); + * Bookmarks bookmarks = new Bookmarks(); + * <p/> + * // Bookmark a URL + * BookmarkedURL url = new BookmarkedURL(); + * url.setName("Google"); + * url.setURL("http://www.jivesoftware.com"); + * bookmarks.addURL(url); + * // Bookmark a Conference room. + * BookmarkedConference conference = new BookmarkedConference(); + * conference.setName("My Favorite Room"); + * conference.setAutoJoin("true"); + * conference.setJID("dev@conference.jivesoftware.com"); + * bookmarks.addConference(conference); + * // Save Bookmarks using PrivateDataManager. + * PrivateDataManager manager = new PrivateDataManager(con); + * manager.setPrivateData(bookmarks); + * <p/> + * <p/> + * LastActivity activity = LastActivity.getLastActivity(con, "xray@jabber.org"); + * </pre> + * + * @author Derek DeMoro + */ +public class Bookmarks implements PrivateData { + + private List<BookmarkedURL> bookmarkedURLS; + private List<BookmarkedConference> bookmarkedConferences; + + /** + * Required Empty Constructor to use Bookmarks. + */ + public Bookmarks() { + bookmarkedURLS = new ArrayList<BookmarkedURL>(); + bookmarkedConferences = new ArrayList<BookmarkedConference>(); + } + + /** + * Adds a BookmarkedURL. + * + * @param bookmarkedURL the bookmarked bookmarkedURL. + */ + public void addBookmarkedURL(BookmarkedURL bookmarkedURL) { + bookmarkedURLS.add(bookmarkedURL); + } + + /** + * Removes a bookmarked bookmarkedURL. + * + * @param bookmarkedURL the bookmarked bookmarkedURL to remove. + */ + public void removeBookmarkedURL(BookmarkedURL bookmarkedURL) { + bookmarkedURLS.remove(bookmarkedURL); + } + + /** + * Removes all BookmarkedURLs from user's bookmarks. + */ + public void clearBookmarkedURLS() { + bookmarkedURLS.clear(); + } + + /** + * Add a BookmarkedConference to bookmarks. + * + * @param bookmarkedConference the conference to remove. + */ + public void addBookmarkedConference(BookmarkedConference bookmarkedConference) { + bookmarkedConferences.add(bookmarkedConference); + } + + /** + * Removes a BookmarkedConference. + * + * @param bookmarkedConference the BookmarkedConference to remove. + */ + public void removeBookmarkedConference(BookmarkedConference bookmarkedConference) { + bookmarkedConferences.remove(bookmarkedConference); + } + + /** + * Removes all BookmarkedConferences from Bookmarks. + */ + public void clearBookmarkedConferences() { + bookmarkedConferences.clear(); + } + + /** + * Returns a Collection of all Bookmarked URLs for this user. + * + * @return a collection of all Bookmarked URLs. + */ + public List<BookmarkedURL> getBookmarkedURLS() { + return bookmarkedURLS; + } + + /** + * Returns a Collection of all Bookmarked Conference for this user. + * + * @return a collection of all Bookmarked Conferences. + */ + public List<BookmarkedConference> getBookmarkedConferences() { + return bookmarkedConferences; + } + + + /** + * Returns the root element name. + * + * @return the element name. + */ + public String getElementName() { + return "storage"; + } + + /** + * Returns the root element XML namespace. + * + * @return the namespace. + */ + public String getNamespace() { + return "storage:bookmarks"; + } + + /** + * Returns the XML reppresentation of the PrivateData. + * + * @return the private data as XML. + */ + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<storage xmlns=\"storage:bookmarks\">"); + + final Iterator<BookmarkedURL> urls = getBookmarkedURLS().iterator(); + while (urls.hasNext()) { + BookmarkedURL urlStorage = urls.next(); + if(urlStorage.isShared()) { + continue; + } + buf.append("<url name=\"").append(urlStorage.getName()). + append("\" url=\"").append(urlStorage.getURL()).append("\""); + if(urlStorage.isRss()) { + buf.append(" rss=\"").append(true).append("\""); + } + buf.append(" />"); + } + + // Add Conference additions + final Iterator<BookmarkedConference> conferences = getBookmarkedConferences().iterator(); + while (conferences.hasNext()) { + BookmarkedConference conference = conferences.next(); + if(conference.isShared()) { + continue; + } + buf.append("<conference "); + buf.append("name=\"").append(conference.getName()).append("\" "); + buf.append("autojoin=\"").append(conference.isAutoJoin()).append("\" "); + buf.append("jid=\"").append(conference.getJid()).append("\" "); + buf.append(">"); + + if (conference.getNickname() != null) { + buf.append("<nick>").append(conference.getNickname()).append("</nick>"); + } + + + if (conference.getPassword() != null) { + buf.append("<password>").append(conference.getPassword()).append("</password>"); + } + buf.append("</conference>"); + } + + + buf.append("</storage>"); + return buf.toString(); + } + + /** + * The IQ Provider for BookmarkStorage. + * + * @author Derek DeMoro + */ + public static class Provider implements PrivateDataProvider { + + /** + * Empty Constructor for PrivateDataProvider. + */ + public Provider() { + super(); + } + + public PrivateData parsePrivateData(XmlPullParser parser) throws Exception { + Bookmarks storage = new Bookmarks(); + + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG && "url".equals(parser.getName())) { + final BookmarkedURL urlStorage = getURLStorage(parser); + if (urlStorage != null) { + storage.addBookmarkedURL(urlStorage); + } + } + else if (eventType == XmlPullParser.START_TAG && + "conference".equals(parser.getName())) + { + final BookmarkedConference conference = getConferenceStorage(parser); + storage.addBookmarkedConference(conference); + } + else if (eventType == XmlPullParser.END_TAG && "storage".equals(parser.getName())) + { + done = true; + } + } + + + return storage; + } + } + + private static BookmarkedURL getURLStorage(XmlPullParser parser) throws IOException, XmlPullParserException { + String name = parser.getAttributeValue("", "name"); + String url = parser.getAttributeValue("", "url"); + String rssString = parser.getAttributeValue("", "rss"); + boolean rss = rssString != null && "true".equals(rssString); + + BookmarkedURL urlStore = new BookmarkedURL(url, name, rss); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if(eventType == XmlPullParser.START_TAG + && "shared_bookmark".equals(parser.getName())) { + urlStore.setShared(true); + } + else if (eventType == XmlPullParser.END_TAG && "url".equals(parser.getName())) { + done = true; + } + } + return urlStore; + } + + private static BookmarkedConference getConferenceStorage(XmlPullParser parser) throws Exception { + String name = parser.getAttributeValue("", "name"); + String autojoin = parser.getAttributeValue("", "autojoin"); + String jid = parser.getAttributeValue("", "jid"); + + BookmarkedConference conf = new BookmarkedConference(jid); + conf.setName(name); + conf.setAutoJoin(Boolean.valueOf(autojoin).booleanValue()); + + // Check for nickname + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG && "nick".equals(parser.getName())) { + conf.setNickname(parser.nextText()); + } + else if (eventType == XmlPullParser.START_TAG && "password".equals(parser.getName())) { + conf.setPassword(parser.nextText()); + } + else if(eventType == XmlPullParser.START_TAG + && "shared_bookmark".equals(parser.getName())) { + conf.setShared(true); + } + else if (eventType == XmlPullParser.END_TAG && "conference".equals(parser.getName())) { + done = true; + } + } + + + return conf; + } +} diff --git a/src/org/jivesoftware/smackx/bookmark/SharedBookmark.java b/src/org/jivesoftware/smackx/bookmark/SharedBookmark.java new file mode 100644 index 0000000..f672bc1 --- /dev/null +++ b/src/org/jivesoftware/smackx/bookmark/SharedBookmark.java @@ -0,0 +1,35 @@ +/** + * $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.smackx.bookmark; + +/** + * Interface to indicate if a bookmark is shared across the server. + * + * @author Alexander Wenckus + */ +public interface SharedBookmark { + + /** + * Returns true if this bookmark is shared. + * + * @return returns true if this bookmark is shared. + */ + public boolean isShared(); +} diff --git a/src/org/jivesoftware/smackx/bytestreams/BytestreamListener.java b/src/org/jivesoftware/smackx/bytestreams/BytestreamListener.java new file mode 100644 index 0000000..be78255 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/BytestreamListener.java @@ -0,0 +1,47 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams;
+
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamListener;
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamListener;
+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;
+
+/**
+ * BytestreamListener are notified if a remote user wants to initiate a bytestream. Implement this
+ * interface to handle incoming bytestream requests.
+ * <p>
+ * BytestreamListener can be registered at the {@link Socks5BytestreamManager} or the
+ * {@link InBandBytestreamManager}.
+ * <p>
+ * There are two ways to add this listener. See
+ * {@link BytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
+ * {@link BytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for further
+ * details.
+ * <p>
+ * {@link Socks5BytestreamListener} or {@link InBandBytestreamListener} provide a more specific
+ * interface of the BytestreamListener.
+ *
+ * @author Henning Staib
+ */
+public interface BytestreamListener {
+
+ /**
+ * This listener is notified if a bytestream request from another user has been received.
+ *
+ * @param request the incoming bytestream request
+ */
+ public void incomingBytestreamRequest(BytestreamRequest request);
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/BytestreamManager.java b/src/org/jivesoftware/smackx/bytestreams/BytestreamManager.java new file mode 100644 index 0000000..ca6bbc6 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/BytestreamManager.java @@ -0,0 +1,114 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams;
+
+import java.io.IOException;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;
+
+/**
+ * BytestreamManager provides a generic interface for bytestream managers.
+ * <p>
+ * There are two implementations of the interface. See {@link Socks5BytestreamManager} and
+ * {@link InBandBytestreamManager}.
+ *
+ * @author Henning Staib
+ */
+public interface BytestreamManager {
+
+ /**
+ * Adds {@link BytestreamListener} that is called for every incoming bytestream request unless
+ * there is a user specific {@link BytestreamListener} registered.
+ * <p>
+ * See {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
+ * {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener)} for further
+ * details.
+ *
+ * @param listener the listener to register
+ */
+ public void addIncomingBytestreamListener(BytestreamListener listener);
+
+ /**
+ * Removes the given listener from the list of listeners for all incoming bytestream requests.
+ *
+ * @param listener the listener to remove
+ */
+ public void removeIncomingBytestreamListener(BytestreamListener listener);
+
+ /**
+ * Adds {@link BytestreamListener} that is called for every incoming bytestream request unless
+ * there is a user specific {@link BytestreamListener} registered.
+ * <p>
+ * Use this method if you are awaiting an incoming bytestream request from a specific user.
+ * <p>
+ * See {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)}
+ * and {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)}
+ * for further details.
+ *
+ * @param listener the listener to register
+ * @param initiatorJID the JID of the user that wants to establish a bytestream
+ */
+ public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID);
+
+ /**
+ * Removes the listener for the given user.
+ *
+ * @param initiatorJID the JID of the user the listener should be removed
+ */
+ public void removeIncomingBytestreamListener(String initiatorJID);
+
+ /**
+ * Establishes a bytestream with the given user and returns the session to send/receive data
+ * to/from the user.
+ * <p>
+ * Use this method to establish bytestreams to users accepting all incoming bytestream requests
+ * since this method doesn't provide a way to tell the user something about the data to be sent.
+ * <p>
+ * To establish a bytestream after negotiation the kind of data to be sent (e.g. file transfer)
+ * use {@link #establishSession(String, String)}.
+ * <p>
+ * See {@link Socks5BytestreamManager#establishSession(String)} and
+ * {@link InBandBytestreamManager#establishSession(String)} for further details.
+ *
+ * @param targetJID the JID of the user a bytestream should be established
+ * @return the session to send/receive data to/from the user
+ * @throws XMPPException if an error occurred while establishing the session
+ * @throws IOException if an IO error occurred while establishing the session
+ * @throws InterruptedException if the thread was interrupted while waiting in a blocking
+ * operation
+ */
+ public BytestreamSession establishSession(String targetJID) throws XMPPException, IOException,
+ InterruptedException;
+
+ /**
+ * Establishes a bytestream with the given user and returns the session to send/receive data
+ * to/from the user.
+ * <p>
+ * See {@link Socks5BytestreamManager#establishSession(String)} and
+ * {@link InBandBytestreamManager#establishSession(String)} for further details.
+ *
+ * @param targetJID the JID of the user a bytestream should be established
+ * @param sessionID the session ID for the bytestream request
+ * @return the session to send/receive data to/from the user
+ * @throws XMPPException if an error occurred while establishing the session
+ * @throws IOException if an IO error occurred while establishing the session
+ * @throws InterruptedException if the thread was interrupted while waiting in a blocking
+ * operation
+ */
+ public BytestreamSession establishSession(String targetJID, String sessionID)
+ throws XMPPException, IOException, InterruptedException;
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/BytestreamRequest.java b/src/org/jivesoftware/smackx/bytestreams/BytestreamRequest.java new file mode 100644 index 0000000..e368bad --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/BytestreamRequest.java @@ -0,0 +1,59 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamRequest;
+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamRequest;
+
+/**
+ * BytestreamRequest provides an interface to handle incoming bytestream requests.
+ * <p>
+ * There are two implementations of the interface. See {@link Socks5BytestreamRequest} and
+ * {@link InBandBytestreamRequest}.
+ *
+ * @author Henning Staib
+ */
+public interface BytestreamRequest {
+
+ /**
+ * Returns the sender of the bytestream open request.
+ *
+ * @return the sender of the bytestream open request
+ */
+ public String getFrom();
+
+ /**
+ * Returns the session ID of the bytestream open request.
+ *
+ * @return the session ID of the bytestream open request
+ */
+ public String getSessionID();
+
+ /**
+ * Accepts the bytestream open request and returns the session to send/receive data.
+ *
+ * @return the session to send/receive data
+ * @throws XMPPException if an error occurred while accepting the bytestream request
+ * @throws InterruptedException if the thread was interrupted while waiting in a blocking
+ * operation
+ */
+ public BytestreamSession accept() throws XMPPException, InterruptedException;
+
+ /**
+ * Rejects the bytestream request by sending a reject error to the initiator.
+ */
+ public void reject();
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/BytestreamSession.java b/src/org/jivesoftware/smackx/bytestreams/BytestreamSession.java new file mode 100644 index 0000000..7aafc35 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/BytestreamSession.java @@ -0,0 +1,81 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamSession;
+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamSession;
+
+/**
+ * BytestreamSession provides an interface for established bytestream sessions.
+ * <p>
+ * There are two implementations of the interface. See {@link Socks5BytestreamSession} and
+ * {@link InBandBytestreamSession}.
+ *
+ * @author Henning Staib
+ */
+public interface BytestreamSession {
+
+ /**
+ * Returns the InputStream associated with this session to send data.
+ *
+ * @return the InputStream associated with this session to send data
+ * @throws IOException if an error occurs while retrieving the input stream
+ */
+ public InputStream getInputStream() throws IOException;
+
+ /**
+ * Returns the OutputStream associated with this session to receive data.
+ *
+ * @return the OutputStream associated with this session to receive data
+ * @throws IOException if an error occurs while retrieving the output stream
+ */
+ public OutputStream getOutputStream() throws IOException;
+
+ /**
+ * Closes the bytestream session.
+ * <p>
+ * Closing the session will also close the input stream and the output stream associated to this
+ * session.
+ *
+ * @throws IOException if an error occurs while closing the session
+ */
+ public void close() throws IOException;
+
+ /**
+ * Returns the timeout for read operations of the input stream associated with this session. 0
+ * returns implies that the option is disabled (i.e., timeout of infinity). Default is 0.
+ *
+ * @return the timeout for read operations
+ * @throws IOException if there is an error in the underlying protocol
+ */
+ public int getReadTimeout() throws IOException;
+
+ /**
+ * Sets the specified timeout, in milliseconds. With this option set to a non-zero timeout, a
+ * read() call on the input stream associated with this session will block for only this amount
+ * of time. If the timeout expires, a java.net.SocketTimeoutException is raised, though the
+ * session is still valid. The option must be enabled prior to entering the blocking operation
+ * to have effect. The timeout must be > 0. A timeout of zero is interpreted as an infinite
+ * timeout. Default is 0.
+ *
+ * @param timeout the specified timeout, in milliseconds
+ * @throws IOException if there is an error in the underlying protocol
+ */
+ public void setReadTimeout(int timeout) throws IOException;
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/CloseListener.java b/src/org/jivesoftware/smackx/bytestreams/ibb/CloseListener.java new file mode 100644 index 0000000..7690e95 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/CloseListener.java @@ -0,0 +1,75 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.ibb;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.IQTypeFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;
+
+/**
+ * CloseListener handles all In-Band Bytestream close requests.
+ * <p>
+ * If a close request is received it looks if a stored In-Band Bytestream
+ * session exists and closes it. If no session with the given session ID exists
+ * an <item-not-found/> error is returned to the sender.
+ *
+ * @author Henning Staib
+ */
+class CloseListener implements PacketListener {
+
+ /* manager containing the listeners and the XMPP connection */
+ private final InBandBytestreamManager manager;
+
+ /* packet filter for all In-Band Bytestream close requests */
+ private final PacketFilter closeFilter = new AndFilter(new PacketTypeFilter(
+ Close.class), new IQTypeFilter(IQ.Type.SET));
+
+ /**
+ * Constructor.
+ *
+ * @param manager the In-Band Bytestream manager
+ */
+ protected CloseListener(InBandBytestreamManager manager) {
+ this.manager = manager;
+ }
+
+ public void processPacket(Packet packet) {
+ Close closeRequest = (Close) packet;
+ InBandBytestreamSession ibbSession = this.manager.getSessions().get(
+ closeRequest.getSessionID());
+ if (ibbSession == null) {
+ this.manager.replyItemNotFoundPacket(closeRequest);
+ }
+ else {
+ ibbSession.closeByPeer(closeRequest);
+ this.manager.getSessions().remove(closeRequest.getSessionID());
+ }
+
+ }
+
+ /**
+ * Returns the packet filter for In-Band Bytestream close requests.
+ *
+ * @return the packet filter for In-Band Bytestream close requests
+ */
+ protected PacketFilter getFilter() {
+ return this.closeFilter;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/DataListener.java b/src/org/jivesoftware/smackx/bytestreams/ibb/DataListener.java new file mode 100644 index 0000000..166c146 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/DataListener.java @@ -0,0 +1,73 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.ibb;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.Data;
+
+/**
+ * DataListener handles all In-Band Bytestream IQ stanzas containing a data
+ * packet extension that don't belong to an existing session.
+ * <p>
+ * If a data packet is received it looks if a stored In-Band Bytestream session
+ * exists. If no session with the given session ID exists an
+ * <item-not-found/> error is returned to the sender.
+ * <p>
+ * Data packets belonging to a running In-Band Bytestream session are processed
+ * by more specific listeners registered when an {@link InBandBytestreamSession}
+ * is created.
+ *
+ * @author Henning Staib
+ */
+class DataListener implements PacketListener {
+
+ /* manager containing the listeners and the XMPP connection */
+ private final InBandBytestreamManager manager;
+
+ /* packet filter for all In-Band Bytestream data packets */
+ private final PacketFilter dataFilter = new AndFilter(
+ new PacketTypeFilter(Data.class));
+
+ /**
+ * Constructor.
+ *
+ * @param manager the In-Band Bytestream manager
+ */
+ public DataListener(InBandBytestreamManager manager) {
+ this.manager = manager;
+ }
+
+ public void processPacket(Packet packet) {
+ Data data = (Data) packet;
+ InBandBytestreamSession ibbSession = this.manager.getSessions().get(
+ data.getDataPacketExtension().getSessionID());
+ if (ibbSession == null) {
+ this.manager.replyItemNotFoundPacket(data);
+ }
+ }
+
+ /**
+ * Returns the packet filter for In-Band Bytestream data packets.
+ *
+ * @return the packet filter for In-Band Bytestream data packets
+ */
+ protected PacketFilter getFilter() {
+ return this.dataFilter;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamListener.java b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamListener.java new file mode 100644 index 0000000..68791a6 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamListener.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.smackx.bytestreams.ibb;
+
+import org.jivesoftware.smackx.bytestreams.BytestreamListener;
+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
+
+/**
+ * InBandBytestreamListener are informed if a remote user wants to initiate an In-Band Bytestream.
+ * Implement this interface to handle incoming In-Band Bytestream requests.
+ * <p>
+ * There are two ways to add this listener. See
+ * {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
+ * {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for
+ * further details.
+ *
+ * @author Henning Staib
+ */
+public abstract class InBandBytestreamListener implements BytestreamListener {
+
+
+
+ public void incomingBytestreamRequest(BytestreamRequest request) {
+ incomingBytestreamRequest((InBandBytestreamRequest) request);
+ }
+
+ /**
+ * This listener is notified if an In-Band Bytestream request from another user has been
+ * received.
+ *
+ * @param request the incoming In-Band Bytestream request
+ */
+ public abstract void incomingBytestreamRequest(InBandBytestreamRequest request);
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamManager.java b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamManager.java new file mode 100644 index 0000000..ef52531 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamManager.java @@ -0,0 +1,546 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.ibb;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jivesoftware.smack.AbstractConnectionListener;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.SyncPacketSend;
+import org.jivesoftware.smackx.bytestreams.BytestreamListener;
+import org.jivesoftware.smackx.bytestreams.BytestreamManager;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
+import org.jivesoftware.smackx.filetransfer.FileTransferManager;
+
+/**
+ * The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the <a
+ * href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>.
+ * <p>
+ * The In-Band Bytestreams (IBB) enables two entities to establish a virtual bytestream over which
+ * they can exchange Base64-encoded chunks of data over XMPP itself. It is the fall-back mechanism
+ * in case the Socks5 bytestream method of transferring data is not available.
+ * <p>
+ * There are two ways to send data over an In-Band Bytestream. It could either use IQ stanzas to
+ * send data packets or message stanzas. If IQ stanzas are used every data packet is acknowledged by
+ * the receiver. This is the recommended way to avoid possible rate-limiting penalties. Message
+ * stanzas are not acknowledged because most XMPP server implementation don't support stanza
+ * flow-control method like <a href="http://xmpp.org/extensions/xep-0079.html">Advanced Message
+ * Processing</a>. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}.
+ * <p>
+ * To establish an In-Band Bytestream invoke the {@link #establishSession(String)} method. This will
+ * negotiate an in-band bytestream with the given target JID and return a session.
+ * <p>
+ * If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file
+ * transfer) invoke {@link #establishSession(String, String)}.
+ * <p>
+ * To handle incoming In-Band Bytestream requests add an {@link InBandBytestreamListener} to the
+ * manager. There are two ways to add this listener. If you want to be informed about incoming
+ * In-Band Bytestreams from a specific user add the listener by invoking
+ * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should
+ * respond to all In-Band Bytestream requests invoke
+ * {@link #addIncomingBytestreamListener(BytestreamListener)}.
+ * <p>
+ * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
+ * In-Band bytestream requests sent in the context of <a
+ * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
+ * {@link FileTransferManager})
+ * <p>
+ * If no {@link InBandBytestreamListener}s are registered, all incoming In-Band bytestream requests
+ * will be rejected by returning a <not-acceptable/> error to the initiator.
+ *
+ * @author Henning Staib
+ */
+public class InBandBytestreamManager implements BytestreamManager {
+
+ /**
+ * Stanzas that can be used to encapsulate In-Band Bytestream data packets.
+ */
+ public enum StanzaType {
+
+ /**
+ * IQ stanza.
+ */
+ IQ,
+
+ /**
+ * Message stanza.
+ */
+ MESSAGE
+ }
+
+ /*
+ * create a new InBandBytestreamManager and register its shutdown listener on every established
+ * connection
+ */
+ static {
+ Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+ public void connectionCreated(Connection connection) {
+ final InBandBytestreamManager manager;
+ manager = InBandBytestreamManager.getByteStreamManager(connection);
+
+ // register shutdown listener
+ connection.addConnectionListener(new AbstractConnectionListener() {
+
+ public void connectionClosed() {
+ manager.disableService();
+ }
+
+ });
+
+ }
+ });
+ }
+
+ /**
+ * The XMPP namespace of the In-Band Bytestream
+ */
+ public static final String NAMESPACE = "http://jabber.org/protocol/ibb";
+
+ /**
+ * Maximum block size that is allowed for In-Band Bytestreams
+ */
+ public static final int MAXIMUM_BLOCK_SIZE = 65535;
+
+ /* prefix used to generate session IDs */
+ private static final String SESSION_ID_PREFIX = "jibb_";
+
+ /* random generator to create session IDs */
+ private final static Random randomGenerator = new Random();
+
+ /* stores one InBandBytestreamManager for each XMPP connection */
+ private final static Map<Connection, InBandBytestreamManager> managers = new HashMap<Connection, InBandBytestreamManager>();
+
+ /* XMPP connection */
+ private final Connection connection;
+
+ /*
+ * assigns a user to a listener that is informed if an In-Band Bytestream request for this user
+ * is received
+ */
+ private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
+
+ /*
+ * list of listeners that respond to all In-Band Bytestream requests if there are no user
+ * specific listeners for that request
+ */
+ private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
+
+ /* listener that handles all incoming In-Band Bytestream requests */
+ private final InitiationListener initiationListener;
+
+ /* listener that handles all incoming In-Band Bytestream IQ data packets */
+ private final DataListener dataListener;
+
+ /* listener that handles all incoming In-Band Bytestream close requests */
+ private final CloseListener closeListener;
+
+ /* assigns a session ID to the In-Band Bytestream session */
+ private final Map<String, InBandBytestreamSession> sessions = new ConcurrentHashMap<String, InBandBytestreamSession>();
+
+ /* block size used for new In-Band Bytestreams */
+ private int defaultBlockSize = 4096;
+
+ /* maximum block size allowed for this connection */
+ private int maximumBlockSize = MAXIMUM_BLOCK_SIZE;
+
+ /* the stanza used to send data packets */
+ private StanzaType stanza = StanzaType.IQ;
+
+ /*
+ * list containing session IDs of In-Band Bytestream open packets that should be ignored by the
+ * InitiationListener
+ */
+ private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
+
+ /**
+ * Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given
+ * {@link Connection}.
+ *
+ * @param connection the XMPP connection
+ * @return the InBandBytestreamManager for the given XMPP connection
+ */
+ public static synchronized InBandBytestreamManager getByteStreamManager(Connection connection) {
+ if (connection == null)
+ return null;
+ InBandBytestreamManager manager = managers.get(connection);
+ if (manager == null) {
+ manager = new InBandBytestreamManager(connection);
+ managers.put(connection, manager);
+ }
+ return manager;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param connection the XMPP connection
+ */
+ private InBandBytestreamManager(Connection connection) {
+ this.connection = connection;
+
+ // register bytestream open packet listener
+ this.initiationListener = new InitiationListener(this);
+ this.connection.addPacketListener(this.initiationListener,
+ this.initiationListener.getFilter());
+
+ // register bytestream data packet listener
+ this.dataListener = new DataListener(this);
+ this.connection.addPacketListener(this.dataListener, this.dataListener.getFilter());
+
+ // register bytestream close packet listener
+ this.closeListener = new CloseListener(this);
+ this.connection.addPacketListener(this.closeListener, this.closeListener.getFilter());
+
+ }
+
+ /**
+ * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
+ * unless there is a user specific InBandBytestreamListener registered.
+ * <p>
+ * If no listeners are registered all In-Band Bytestream request are rejected with a
+ * <not-acceptable/> error.
+ * <p>
+ * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
+ * Socks5 bytestream requests sent in the context of <a
+ * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
+ * {@link FileTransferManager})
+ *
+ * @param listener the listener to register
+ */
+ public void addIncomingBytestreamListener(BytestreamListener listener) {
+ this.allRequestListeners.add(listener);
+ }
+
+ /**
+ * Removes the given listener from the list of listeners for all incoming In-Band Bytestream
+ * requests.
+ *
+ * @param listener the listener to remove
+ */
+ public void removeIncomingBytestreamListener(BytestreamListener listener) {
+ this.allRequestListeners.remove(listener);
+ }
+
+ /**
+ * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request
+ * from the given user.
+ * <p>
+ * Use this method if you are awaiting an incoming Socks5 bytestream request from a specific
+ * user.
+ * <p>
+ * If no listeners are registered all In-Band Bytestream request are rejected with a
+ * <not-acceptable/> error.
+ * <p>
+ * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming
+ * Socks5 bytestream requests sent in the context of <a
+ * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
+ * {@link FileTransferManager})
+ *
+ * @param listener the listener to register
+ * @param initiatorJID the JID of the user that wants to establish an In-Band Bytestream
+ */
+ public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {
+ this.userListeners.put(initiatorJID, listener);
+ }
+
+ /**
+ * Removes the listener for the given user.
+ *
+ * @param initiatorJID the JID of the user the listener should be removed
+ */
+ public void removeIncomingBytestreamListener(String initiatorJID) {
+ this.userListeners.remove(initiatorJID);
+ }
+
+ /**
+ * Use this method to ignore the next incoming In-Band Bytestream request containing the given
+ * session ID. No listeners will be notified for this request and and no error will be returned
+ * to the initiator.
+ * <p>
+ * This method should be used if you are awaiting an In-Band Bytestream request as a reply to
+ * another packet (e.g. file transfer).
+ *
+ * @param sessionID to be ignored
+ */
+ public void ignoreBytestreamRequestOnce(String sessionID) {
+ this.ignoredBytestreamRequests.add(sessionID);
+ }
+
+ /**
+ * Returns the default block size that is used for all outgoing in-band bytestreams for this
+ * connection.
+ * <p>
+ * The recommended default block size is 4096 bytes. See <a
+ * href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> Section 5.
+ *
+ * @return the default block size
+ */
+ public int getDefaultBlockSize() {
+ return defaultBlockSize;
+ }
+
+ /**
+ * Sets the default block size that is used for all outgoing in-band bytestreams for this
+ * connection.
+ * <p>
+ * The default block size must be between 1 and 65535 bytes. The recommended default block size
+ * is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a>
+ * Section 5.
+ *
+ * @param defaultBlockSize the default block size to set
+ */
+ public void setDefaultBlockSize(int defaultBlockSize) {
+ if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) {
+ throw new IllegalArgumentException("Default block size must be between 1 and "
+ + MAXIMUM_BLOCK_SIZE);
+ }
+ this.defaultBlockSize = defaultBlockSize;
+ }
+
+ /**
+ * Returns the maximum block size that is allowed for In-Band Bytestreams for this connection.
+ * <p>
+ * Incoming In-Band Bytestream open request will be rejected with an
+ * <resource-constraint/> error if the block size is greater then the maximum allowed
+ * block size.
+ * <p>
+ * The default maximum block size is 65535 bytes.
+ *
+ * @return the maximum block size
+ */
+ public int getMaximumBlockSize() {
+ return maximumBlockSize;
+ }
+
+ /**
+ * Sets the maximum block size that is allowed for In-Band Bytestreams for this connection.
+ * <p>
+ * The maximum block size must be between 1 and 65535 bytes.
+ * <p>
+ * Incoming In-Band Bytestream open request will be rejected with an
+ * <resource-constraint/> error if the block size is greater then the maximum allowed
+ * block size.
+ *
+ * @param maximumBlockSize the maximum block size to set
+ */
+ public void setMaximumBlockSize(int maximumBlockSize) {
+ if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) {
+ throw new IllegalArgumentException("Maximum block size must be between 1 and "
+ + MAXIMUM_BLOCK_SIZE);
+ }
+ this.maximumBlockSize = maximumBlockSize;
+ }
+
+ /**
+ * Returns the stanza used to send data packets.
+ * <p>
+ * Default is {@link StanzaType#IQ}. See <a
+ * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
+ *
+ * @return the stanza used to send data packets
+ */
+ public StanzaType getStanza() {
+ return stanza;
+ }
+
+ /**
+ * Sets the stanza used to send data packets.
+ * <p>
+ * The use of {@link StanzaType#IQ} is recommended. See <a
+ * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.
+ *
+ * @param stanza the stanza to set
+ */
+ public void setStanza(StanzaType stanza) {
+ this.stanza = stanza;
+ }
+
+ /**
+ * Establishes an In-Band Bytestream with the given user and returns the session to send/receive
+ * data to/from the user.
+ * <p>
+ * Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band
+ * Bytestream requests since this method doesn't provide a way to tell the user something about
+ * the data to be sent.
+ * <p>
+ * To establish an In-Band Bytestream after negotiation the kind of data to be sent (e.g. file
+ * transfer) use {@link #establishSession(String, String)}.
+ *
+ * @param targetJID the JID of the user an In-Band Bytestream should be established
+ * @return the session to send/receive data to/from the user
+ * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
+ * user prefers smaller block sizes
+ */
+ public InBandBytestreamSession establishSession(String targetJID) throws XMPPException {
+ String sessionID = getNextSessionID();
+ return establishSession(targetJID, sessionID);
+ }
+
+ /**
+ * Establishes an In-Band Bytestream with the given user using the given session ID and returns
+ * the session to send/receive data to/from the user.
+ *
+ * @param targetJID the JID of the user an In-Band Bytestream should be established
+ * @param sessionID the session ID for the In-Band Bytestream request
+ * @return the session to send/receive data to/from the user
+ * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the
+ * user prefers smaller block sizes
+ */
+ public InBandBytestreamSession establishSession(String targetJID, String sessionID)
+ throws XMPPException {
+ Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza);
+ byteStreamRequest.setTo(targetJID);
+
+ // sending packet will throw exception on timeout or error reply
+ SyncPacketSend.getReply(this.connection, byteStreamRequest);
+
+ InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession(
+ this.connection, byteStreamRequest, targetJID);
+ this.sessions.put(sessionID, inBandBytestreamSession);
+
+ return inBandBytestreamSession;
+ }
+
+ /**
+ * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is
+ * not accepted.
+ *
+ * @param request IQ packet that should be answered with a not-acceptable error
+ */
+ protected void replyRejectPacket(IQ request) {
+ XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
+ IQ error = IQ.createErrorResponse(request, xmppError);
+ this.connection.sendPacket(error);
+ }
+
+ /**
+ * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open
+ * request is rejected because its block size is greater than the maximum allowed block size.
+ *
+ * @param request IQ packet that should be answered with a resource-constraint error
+ */
+ protected void replyResourceConstraintPacket(IQ request) {
+ XMPPError xmppError = new XMPPError(XMPPError.Condition.resource_constraint);
+ IQ error = IQ.createErrorResponse(request, xmppError);
+ this.connection.sendPacket(error);
+ }
+
+ /**
+ * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream
+ * session could not be found.
+ *
+ * @param request IQ packet that should be answered with a item-not-found error
+ */
+ protected void replyItemNotFoundPacket(IQ request) {
+ XMPPError xmppError = new XMPPError(XMPPError.Condition.item_not_found);
+ IQ error = IQ.createErrorResponse(request, xmppError);
+ this.connection.sendPacket(error);
+ }
+
+ /**
+ * Returns a new unique session ID.
+ *
+ * @return a new unique session ID
+ */
+ private String getNextSessionID() {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append(SESSION_ID_PREFIX);
+ buffer.append(Math.abs(randomGenerator.nextLong()));
+ return buffer.toString();
+ }
+
+ /**
+ * Returns the XMPP connection.
+ *
+ * @return the XMPP connection
+ */
+ protected Connection getConnection() {
+ return this.connection;
+ }
+
+ /**
+ * Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream
+ * request from the given initiator JID is received.
+ *
+ * @param initiator the initiator's JID
+ * @return the listener
+ */
+ protected BytestreamListener getUserListener(String initiator) {
+ return this.userListeners.get(initiator);
+ }
+
+ /**
+ * Returns a list of {@link InBandBytestreamListener} that are informed if there are no
+ * listeners for a specific initiator.
+ *
+ * @return list of listeners
+ */
+ protected List<BytestreamListener> getAllRequestListeners() {
+ return this.allRequestListeners;
+ }
+
+ /**
+ * Returns the sessions map.
+ *
+ * @return the sessions map
+ */
+ protected Map<String, InBandBytestreamSession> getSessions() {
+ return sessions;
+ }
+
+ /**
+ * Returns the list of session IDs that should be ignored by the InitialtionListener
+ *
+ * @return list of session IDs
+ */
+ protected List<String> getIgnoredBytestreamRequests() {
+ return ignoredBytestreamRequests;
+ }
+
+ /**
+ * Disables the InBandBytestreamManager by removing its packet listeners and resetting its
+ * internal status.
+ */
+ private void disableService() {
+
+ // remove manager from static managers map
+ managers.remove(connection);
+
+ // remove all listeners registered by this manager
+ this.connection.removePacketListener(this.initiationListener);
+ this.connection.removePacketListener(this.dataListener);
+ this.connection.removePacketListener(this.closeListener);
+
+ // shutdown threads
+ this.initiationListener.shutdown();
+
+ // reset internal status
+ this.userListeners.clear();
+ this.allRequestListeners.clear();
+ this.sessions.clear();
+ this.ignoredBytestreamRequests.clear();
+
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamRequest.java b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamRequest.java new file mode 100644 index 0000000..5bc689a --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamRequest.java @@ -0,0 +1,92 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.ibb;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
+
+/**
+ * InBandBytestreamRequest class handles incoming In-Band Bytestream requests.
+ *
+ * @author Henning Staib
+ */
+public class InBandBytestreamRequest implements BytestreamRequest {
+
+ /* the bytestream initialization request */
+ private final Open byteStreamRequest;
+
+ /*
+ * In-Band Bytestream manager containing the XMPP connection and helper
+ * methods
+ */
+ private final InBandBytestreamManager manager;
+
+ protected InBandBytestreamRequest(InBandBytestreamManager manager,
+ Open byteStreamRequest) {
+ this.manager = manager;
+ this.byteStreamRequest = byteStreamRequest;
+ }
+
+ /**
+ * Returns the sender of the In-Band Bytestream open request.
+ *
+ * @return the sender of the In-Band Bytestream open request
+ */
+ public String getFrom() {
+ return this.byteStreamRequest.getFrom();
+ }
+
+ /**
+ * Returns the session ID of the In-Band Bytestream open request.
+ *
+ * @return the session ID of the In-Band Bytestream open request
+ */
+ public String getSessionID() {
+ return this.byteStreamRequest.getSessionID();
+ }
+
+ /**
+ * Accepts the In-Band Bytestream open request and returns the session to
+ * send/receive data.
+ *
+ * @return the session to send/receive data
+ * @throws XMPPException if stream is invalid.
+ */
+ public InBandBytestreamSession accept() throws XMPPException {
+ Connection connection = this.manager.getConnection();
+
+ // create In-Band Bytestream session and store it
+ InBandBytestreamSession ibbSession = new InBandBytestreamSession(connection,
+ this.byteStreamRequest, this.byteStreamRequest.getFrom());
+ this.manager.getSessions().put(this.byteStreamRequest.getSessionID(), ibbSession);
+
+ // acknowledge request
+ IQ resultIQ = IQ.createResultIQ(this.byteStreamRequest);
+ connection.sendPacket(resultIQ);
+
+ return ibbSession;
+ }
+
+ /**
+ * Rejects the In-Band Bytestream request by sending a reject error to the
+ * initiator.
+ */
+ public void reject() {
+ this.manager.replyRejectPacket(this.byteStreamRequest);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSession.java b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSession.java new file mode 100644 index 0000000..a33682c --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSession.java @@ -0,0 +1,795 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.ibb;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.SocketTimeoutException;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+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.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smack.util.SyncPacketSend;
+import org.jivesoftware.smackx.bytestreams.BytestreamSession;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.Data;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
+
+/**
+ * InBandBytestreamSession class represents an In-Band Bytestream session.
+ * <p>
+ * In-band bytestreams are bidirectional and this session encapsulates the streams for both
+ * directions.
+ * <p>
+ * Note that closing the In-Band Bytestream session will close both streams. If both streams are
+ * closed individually the session will be closed automatically once the second stream is closed.
+ * Use the {@link #setCloseBothStreamsEnabled(boolean)} method if both streams should be closed
+ * automatically if one of them is closed.
+ *
+ * @author Henning Staib
+ */
+public class InBandBytestreamSession implements BytestreamSession {
+
+ /* XMPP connection */
+ private final Connection connection;
+
+ /* the In-Band Bytestream open request for this session */
+ private final Open byteStreamRequest;
+
+ /*
+ * the input stream for this session (either IQIBBInputStream or MessageIBBInputStream)
+ */
+ private IBBInputStream inputStream;
+
+ /*
+ * the output stream for this session (either IQIBBOutputStream or MessageIBBOutputStream)
+ */
+ private IBBOutputStream outputStream;
+
+ /* JID of the remote peer */
+ private String remoteJID;
+
+ /* flag to close both streams if one of them is closed */
+ private boolean closeBothStreamsEnabled = false;
+
+ /* flag to indicate if session is closed */
+ private boolean isClosed = false;
+
+ /**
+ * Constructor.
+ *
+ * @param connection the XMPP connection
+ * @param byteStreamRequest the In-Band Bytestream open request for this session
+ * @param remoteJID JID of the remote peer
+ */
+ protected InBandBytestreamSession(Connection connection, Open byteStreamRequest,
+ String remoteJID) {
+ this.connection = connection;
+ this.byteStreamRequest = byteStreamRequest;
+ this.remoteJID = remoteJID;
+
+ // initialize streams dependent to the uses stanza type
+ switch (byteStreamRequest.getStanza()) {
+ case IQ:
+ this.inputStream = new IQIBBInputStream();
+ this.outputStream = new IQIBBOutputStream();
+ break;
+ case MESSAGE:
+ this.inputStream = new MessageIBBInputStream();
+ this.outputStream = new MessageIBBOutputStream();
+ break;
+ }
+
+ }
+
+ public InputStream getInputStream() {
+ return this.inputStream;
+ }
+
+ public OutputStream getOutputStream() {
+ return this.outputStream;
+ }
+
+ public int getReadTimeout() {
+ return this.inputStream.readTimeout;
+ }
+
+ public void setReadTimeout(int timeout) {
+ if (timeout < 0) {
+ throw new IllegalArgumentException("Timeout must be >= 0");
+ }
+ this.inputStream.readTimeout = timeout;
+ }
+
+ /**
+ * Returns whether both streams should be closed automatically if one of the streams is closed.
+ * Default is <code>false</code>.
+ *
+ * @return <code>true</code> if both streams will be closed if one of the streams is closed,
+ * <code>false</code> if both streams can be closed independently.
+ */
+ public boolean isCloseBothStreamsEnabled() {
+ return closeBothStreamsEnabled;
+ }
+
+ /**
+ * Sets whether both streams should be closed automatically if one of the streams is closed.
+ * Default is <code>false</code>.
+ *
+ * @param closeBothStreamsEnabled <code>true</code> if both streams should be closed if one of
+ * the streams is closed, <code>false</code> if both streams should be closed
+ * independently
+ */
+ public void setCloseBothStreamsEnabled(boolean closeBothStreamsEnabled) {
+ this.closeBothStreamsEnabled = closeBothStreamsEnabled;
+ }
+
+ public void close() throws IOException {
+ closeByLocal(true); // close input stream
+ closeByLocal(false); // close output stream
+ }
+
+ /**
+ * This method is invoked if a request to close the In-Band Bytestream has been received.
+ *
+ * @param closeRequest the close request from the remote peer
+ */
+ protected void closeByPeer(Close closeRequest) {
+
+ /*
+ * close streams without flushing them, because stream is already considered closed on the
+ * remote peers side
+ */
+ this.inputStream.closeInternal();
+ this.inputStream.cleanup();
+ this.outputStream.closeInternal(false);
+
+ // acknowledge close request
+ IQ confirmClose = IQ.createResultIQ(closeRequest);
+ this.connection.sendPacket(confirmClose);
+
+ }
+
+ /**
+ * This method is invoked if one of the streams has been closed locally, if an error occurred
+ * locally or if the whole session should be closed.
+ *
+ * @throws IOException if an error occurs while sending the close request
+ */
+ protected synchronized void closeByLocal(boolean in) throws IOException {
+ if (this.isClosed) {
+ return;
+ }
+
+ if (this.closeBothStreamsEnabled) {
+ this.inputStream.closeInternal();
+ this.outputStream.closeInternal(true);
+ }
+ else {
+ if (in) {
+ this.inputStream.closeInternal();
+ }
+ else {
+ // close stream but try to send any data left
+ this.outputStream.closeInternal(true);
+ }
+ }
+
+ if (this.inputStream.isClosed && this.outputStream.isClosed) {
+ this.isClosed = true;
+
+ // send close request
+ Close close = new Close(this.byteStreamRequest.getSessionID());
+ close.setTo(this.remoteJID);
+ try {
+ SyncPacketSend.getReply(this.connection, close);
+ }
+ catch (XMPPException e) {
+ throw new IOException("Error while closing stream: " + e.getMessage());
+ }
+
+ this.inputStream.cleanup();
+
+ // remove session from manager
+ InBandBytestreamManager.getByteStreamManager(this.connection).getSessions().remove(this);
+ }
+
+ }
+
+ /**
+ * IBBInputStream class is the base implementation of an In-Band Bytestream input stream.
+ * Subclasses of this input stream must provide a packet listener along with a packet filter to
+ * collect the In-Band Bytestream data packets.
+ */
+ private abstract class IBBInputStream extends InputStream {
+
+ /* the data packet listener to fill the data queue */
+ private final PacketListener dataPacketListener;
+
+ /* queue containing received In-Band Bytestream data packets */
+ protected final BlockingQueue<DataPacketExtension> dataQueue = new LinkedBlockingQueue<DataPacketExtension>();
+
+ /* buffer containing the data from one data packet */
+ private byte[] buffer;
+
+ /* pointer to the next byte to read from buffer */
+ private int bufferPointer = -1;
+
+ /* data packet sequence (range from 0 to 65535) */
+ private long seq = -1;
+
+ /* flag to indicate if input stream is closed */
+ private boolean isClosed = false;
+
+ /* flag to indicate if close method was invoked */
+ private boolean closeInvoked = false;
+
+ /* timeout for read operations */
+ private int readTimeout = 0;
+
+ /**
+ * Constructor.
+ */
+ public IBBInputStream() {
+ // add data packet listener to connection
+ this.dataPacketListener = getDataPacketListener();
+ connection.addPacketListener(this.dataPacketListener, getDataPacketFilter());
+ }
+
+ /**
+ * Returns the packet listener that processes In-Band Bytestream data packets.
+ *
+ * @return the data packet listener
+ */
+ protected abstract PacketListener getDataPacketListener();
+
+ /**
+ * Returns the packet filter that accepts In-Band Bytestream data packets.
+ *
+ * @return the data packet filter
+ */
+ protected abstract PacketFilter getDataPacketFilter();
+
+ public synchronized int read() throws IOException {
+ checkClosed();
+
+ // if nothing read yet or whole buffer has been read fill buffer
+ if (bufferPointer == -1 || bufferPointer >= buffer.length) {
+ // if no data available and stream was closed return -1
+ if (!loadBuffer()) {
+ return -1;
+ }
+ }
+
+ // return byte and increment buffer pointer
+ return ((int) buffer[bufferPointer++]) & 0xff;
+ }
+
+ public synchronized int read(byte[] b, int off, int len) throws IOException {
+ if (b == null) {
+ throw new NullPointerException();
+ }
+ else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)
+ || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ }
+ else if (len == 0) {
+ return 0;
+ }
+
+ checkClosed();
+
+ // if nothing read yet or whole buffer has been read fill buffer
+ if (bufferPointer == -1 || bufferPointer >= buffer.length) {
+ // if no data available and stream was closed return -1
+ if (!loadBuffer()) {
+ return -1;
+ }
+ }
+
+ // if more bytes wanted than available return all available
+ int bytesAvailable = buffer.length - bufferPointer;
+ if (len > bytesAvailable) {
+ len = bytesAvailable;
+ }
+
+ System.arraycopy(buffer, bufferPointer, b, off, len);
+ bufferPointer += len;
+ return len;
+ }
+
+ public synchronized int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ /**
+ * This method blocks until a data packet is received, the stream is closed or the current
+ * thread is interrupted.
+ *
+ * @return <code>true</code> if data was received, otherwise <code>false</code>
+ * @throws IOException if data packets are out of sequence
+ */
+ private synchronized boolean loadBuffer() throws IOException {
+
+ // wait until data is available or stream is closed
+ DataPacketExtension data = null;
+ try {
+ if (this.readTimeout == 0) {
+ while (data == null) {
+ if (isClosed && this.dataQueue.isEmpty()) {
+ return false;
+ }
+ data = this.dataQueue.poll(1000, TimeUnit.MILLISECONDS);
+ }
+ }
+ else {
+ data = this.dataQueue.poll(this.readTimeout, TimeUnit.MILLISECONDS);
+ if (data == null) {
+ throw new SocketTimeoutException();
+ }
+ }
+ }
+ catch (InterruptedException e) {
+ // Restore the interrupted status
+ Thread.currentThread().interrupt();
+ return false;
+ }
+
+ // handle sequence overflow
+ if (this.seq == 65535) {
+ this.seq = -1;
+ }
+
+ // check if data packets sequence is successor of last seen sequence
+ long seq = data.getSeq();
+ if (seq - 1 != this.seq) {
+ // packets out of order; close stream/session
+ InBandBytestreamSession.this.close();
+ throw new IOException("Packets out of sequence");
+ }
+ else {
+ this.seq = seq;
+ }
+
+ // set buffer to decoded data
+ buffer = data.getDecodedData();
+ bufferPointer = 0;
+ return true;
+ }
+
+ /**
+ * Checks if this stream is closed and throws an IOException if necessary
+ *
+ * @throws IOException if stream is closed and no data should be read anymore
+ */
+ private void checkClosed() throws IOException {
+ /* throw no exception if there is data available, but not if close method was invoked */
+ if ((isClosed && this.dataQueue.isEmpty()) || closeInvoked) {
+ // clear data queue in case additional data was received after stream was closed
+ this.dataQueue.clear();
+ throw new IOException("Stream is closed");
+ }
+ }
+
+ public boolean markSupported() {
+ return false;
+ }
+
+ public void close() throws IOException {
+ if (isClosed) {
+ return;
+ }
+
+ this.closeInvoked = true;
+
+ InBandBytestreamSession.this.closeByLocal(true);
+ }
+
+ /**
+ * This method sets the close flag and removes the data packet listener.
+ */
+ private void closeInternal() {
+ if (isClosed) {
+ return;
+ }
+ isClosed = true;
+ }
+
+ /**
+ * Invoked if the session is closed.
+ */
+ private void cleanup() {
+ connection.removePacketListener(this.dataPacketListener);
+ }
+
+ }
+
+ /**
+ * IQIBBInputStream class implements IBBInputStream to be used with IQ stanzas encapsulating the
+ * data packets.
+ */
+ private class IQIBBInputStream extends IBBInputStream {
+
+ protected PacketListener getDataPacketListener() {
+ return new PacketListener() {
+
+ private long lastSequence = -1;
+
+ public void processPacket(Packet packet) {
+ // get data packet extension
+ DataPacketExtension data = (DataPacketExtension) packet.getExtension(
+ DataPacketExtension.ELEMENT_NAME,
+ InBandBytestreamManager.NAMESPACE);
+
+ /*
+ * check if sequence was not used already (see XEP-0047 Section 2.2)
+ */
+ if (data.getSeq() <= this.lastSequence) {
+ IQ unexpectedRequest = IQ.createErrorResponse((IQ) packet, new XMPPError(
+ XMPPError.Condition.unexpected_request));
+ connection.sendPacket(unexpectedRequest);
+ return;
+
+ }
+
+ // check if encoded data is valid (see XEP-0047 Section 2.2)
+ if (data.getDecodedData() == null) {
+ // data is invalid; respond with bad-request error
+ IQ badRequest = IQ.createErrorResponse((IQ) packet, new XMPPError(
+ XMPPError.Condition.bad_request));
+ connection.sendPacket(badRequest);
+ return;
+ }
+
+ // data is valid; add to data queue
+ dataQueue.offer(data);
+
+ // confirm IQ
+ IQ confirmData = IQ.createResultIQ((IQ) packet);
+ connection.sendPacket(confirmData);
+
+ // set last seen sequence
+ this.lastSequence = data.getSeq();
+ if (this.lastSequence == 65535) {
+ this.lastSequence = -1;
+ }
+
+ }
+
+ };
+ }
+
+ protected PacketFilter getDataPacketFilter() {
+ /*
+ * filter all IQ stanzas having type 'SET' (represented by Data class), containing a
+ * data packet extension, matching session ID and recipient
+ */
+ return new AndFilter(new PacketTypeFilter(Data.class), new IBBDataPacketFilter());
+ }
+
+ }
+
+ /**
+ * MessageIBBInputStream class implements IBBInputStream to be used with message stanzas
+ * encapsulating the data packets.
+ */
+ private class MessageIBBInputStream extends IBBInputStream {
+
+ protected PacketListener getDataPacketListener() {
+ return new PacketListener() {
+
+ public void processPacket(Packet packet) {
+ // get data packet extension
+ DataPacketExtension data = (DataPacketExtension) packet.getExtension(
+ DataPacketExtension.ELEMENT_NAME,
+ InBandBytestreamManager.NAMESPACE);
+
+ // check if encoded data is valid
+ if (data.getDecodedData() == null) {
+ /*
+ * TODO once a majority of XMPP server implementation support XEP-0079
+ * Advanced Message Processing the invalid message could be answered with an
+ * appropriate error. For now we just ignore the packet. Subsequent packets
+ * with an increased sequence will cause the input stream to close the
+ * stream/session.
+ */
+ return;
+ }
+
+ // data is valid; add to data queue
+ dataQueue.offer(data);
+
+ // TODO confirm packet once XMPP servers support XEP-0079
+ }
+
+ };
+ }
+
+ @Override
+ protected PacketFilter getDataPacketFilter() {
+ /*
+ * filter all message stanzas containing a data packet extension, matching session ID
+ * and recipient
+ */
+ return new AndFilter(new PacketTypeFilter(Message.class), new IBBDataPacketFilter());
+ }
+
+ }
+
+ /**
+ * IBBDataPacketFilter class filters all packets from the remote peer of this session,
+ * containing an In-Band Bytestream data packet extension whose session ID matches this sessions
+ * ID.
+ */
+ private class IBBDataPacketFilter implements PacketFilter {
+
+ public boolean accept(Packet packet) {
+ // sender equals remote peer
+ if (!packet.getFrom().equalsIgnoreCase(remoteJID)) {
+ return false;
+ }
+
+ // stanza contains data packet extension
+ PacketExtension packetExtension = packet.getExtension(DataPacketExtension.ELEMENT_NAME,
+ InBandBytestreamManager.NAMESPACE);
+ if (packetExtension == null || !(packetExtension instanceof DataPacketExtension)) {
+ return false;
+ }
+
+ // session ID equals this session ID
+ DataPacketExtension data = (DataPacketExtension) packetExtension;
+ if (!data.getSessionID().equals(byteStreamRequest.getSessionID())) {
+ return false;
+ }
+
+ return true;
+ }
+
+ }
+
+ /**
+ * IBBOutputStream class is the base implementation of an In-Band Bytestream output stream.
+ * Subclasses of this output stream must provide a method to send data over XMPP stream.
+ */
+ private abstract class IBBOutputStream extends OutputStream {
+
+ /* buffer with the size of this sessions block size */
+ protected final byte[] buffer;
+
+ /* pointer to next byte to write to buffer */
+ protected int bufferPointer = 0;
+
+ /* data packet sequence (range from 0 to 65535) */
+ protected long seq = 0;
+
+ /* flag to indicate if output stream is closed */
+ protected boolean isClosed = false;
+
+ /**
+ * Constructor.
+ */
+ public IBBOutputStream() {
+ this.buffer = new byte[(byteStreamRequest.getBlockSize()/4)*3];
+ }
+
+ /**
+ * Writes the given data packet to the XMPP stream.
+ *
+ * @param data the data packet
+ * @throws IOException if an I/O error occurred while sending or if the stream is closed
+ */
+ protected abstract void writeToXML(DataPacketExtension data) throws IOException;
+
+ public synchronized void write(int b) throws IOException {
+ if (this.isClosed) {
+ throw new IOException("Stream is closed");
+ }
+
+ // if buffer is full flush buffer
+ if (bufferPointer >= buffer.length) {
+ flushBuffer();
+ }
+
+ buffer[bufferPointer++] = (byte) b;
+ }
+
+ public synchronized void write(byte b[], int off, int len) throws IOException {
+ if (b == null) {
+ throw new NullPointerException();
+ }
+ else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)
+ || ((off + len) < 0)) {
+ throw new IndexOutOfBoundsException();
+ }
+ else if (len == 0) {
+ return;
+ }
+
+ if (this.isClosed) {
+ throw new IOException("Stream is closed");
+ }
+
+ // is data to send greater than buffer size
+ if (len >= buffer.length) {
+
+ // "byte" off the first chunk to write out
+ writeOut(b, off, buffer.length);
+
+ // recursively call this method with the lesser amount
+ write(b, off + buffer.length, len - buffer.length);
+ }
+ else {
+ writeOut(b, off, len);
+ }
+ }
+
+ public synchronized void write(byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ /**
+ * Fills the buffer with the given data and sends it over the XMPP stream if the buffers
+ * capacity has been reached. This method is only called from this class so it is assured
+ * that the amount of data to send is <= buffer capacity
+ *
+ * @param b the data
+ * @param off the data
+ * @param len the number of bytes to write
+ * @throws IOException if an I/O error occurred while sending or if the stream is closed
+ */
+ private synchronized void writeOut(byte b[], int off, int len) throws IOException {
+ if (this.isClosed) {
+ throw new IOException("Stream is closed");
+ }
+
+ // set to 0 in case the next 'if' block is not executed
+ int available = 0;
+
+ // is data to send greater that buffer space left
+ if (len > buffer.length - bufferPointer) {
+ // fill buffer to capacity and send it
+ available = buffer.length - bufferPointer;
+ System.arraycopy(b, off, buffer, bufferPointer, available);
+ bufferPointer += available;
+ flushBuffer();
+ }
+
+ // copy the data left to buffer
+ System.arraycopy(b, off + available, buffer, bufferPointer, len - available);
+ bufferPointer += len - available;
+ }
+
+ public synchronized void flush() throws IOException {
+ if (this.isClosed) {
+ throw new IOException("Stream is closed");
+ }
+ flushBuffer();
+ }
+
+ private synchronized void flushBuffer() throws IOException {
+
+ // do nothing if no data to send available
+ if (bufferPointer == 0) {
+ return;
+ }
+
+ // create data packet
+ String enc = StringUtils.encodeBase64(buffer, 0, bufferPointer, false);
+ DataPacketExtension data = new DataPacketExtension(byteStreamRequest.getSessionID(),
+ this.seq, enc);
+
+ // write to XMPP stream
+ writeToXML(data);
+
+ // reset buffer pointer
+ bufferPointer = 0;
+
+ // increment sequence, considering sequence overflow
+ this.seq = (this.seq + 1 == 65535 ? 0 : this.seq + 1);
+
+ }
+
+ public void close() throws IOException {
+ if (isClosed) {
+ return;
+ }
+ InBandBytestreamSession.this.closeByLocal(false);
+ }
+
+ /**
+ * Sets the close flag and optionally flushes the stream.
+ *
+ * @param flush if <code>true</code> flushes the stream
+ */
+ protected void closeInternal(boolean flush) {
+ if (this.isClosed) {
+ return;
+ }
+ this.isClosed = true;
+
+ try {
+ if (flush) {
+ flushBuffer();
+ }
+ }
+ catch (IOException e) {
+ /*
+ * ignore, because writeToXML() will not throw an exception if stream is already
+ * closed
+ */
+ }
+ }
+
+ }
+
+ /**
+ * IQIBBOutputStream class implements IBBOutputStream to be used with IQ stanzas encapsulating
+ * the data packets.
+ */
+ private class IQIBBOutputStream extends IBBOutputStream {
+
+ @Override
+ protected synchronized void writeToXML(DataPacketExtension data) throws IOException {
+ // create IQ stanza containing data packet
+ IQ iq = new Data(data);
+ iq.setTo(remoteJID);
+
+ try {
+ SyncPacketSend.getReply(connection, iq);
+ }
+ catch (XMPPException e) {
+ // close session unless it is already closed
+ if (!this.isClosed) {
+ InBandBytestreamSession.this.close();
+ throw new IOException("Error while sending Data: " + e.getMessage());
+ }
+ }
+
+ }
+
+ }
+
+ /**
+ * MessageIBBOutputStream class implements IBBOutputStream to be used with message stanzas
+ * encapsulating the data packets.
+ */
+ private class MessageIBBOutputStream extends IBBOutputStream {
+
+ @Override
+ protected synchronized void writeToXML(DataPacketExtension data) {
+ // create message stanza containing data packet
+ Message message = new Message(remoteJID);
+ message.addExtension(data);
+
+ connection.sendPacket(message);
+
+ }
+
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/InitiationListener.java b/src/org/jivesoftware/smackx/bytestreams/ibb/InitiationListener.java new file mode 100644 index 0000000..0ecb081 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/InitiationListener.java @@ -0,0 +1,127 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.ibb;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.IQTypeFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.bytestreams.BytestreamListener;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
+
+/**
+ * InitiationListener handles all incoming In-Band Bytestream open requests. If there are no
+ * listeners for a In-Band Bytestream request InitiationListener will always refuse the request and
+ * reply with a <not-acceptable/> error (<a
+ * href="http://xmpp.org/extensions/xep-0047.html#example-5" >XEP-0047</a> Section 2.1).
+ * <p>
+ * All In-Band Bytestream request having a block size greater than the maximum allowed block size
+ * for this connection are rejected with an <resource-constraint/> error. The maximum block
+ * size can be set by invoking {@link InBandBytestreamManager#setMaximumBlockSize(int)}.
+ *
+ * @author Henning Staib
+ */
+class InitiationListener implements PacketListener {
+
+ /* manager containing the listeners and the XMPP connection */
+ private final InBandBytestreamManager manager;
+
+ /* packet filter for all In-Band Bytestream requests */
+ private final PacketFilter initFilter = new AndFilter(new PacketTypeFilter(Open.class),
+ new IQTypeFilter(IQ.Type.SET));
+
+ /* executor service to process incoming requests concurrently */
+ private final ExecutorService initiationListenerExecutor;
+
+ /**
+ * Constructor.
+ *
+ * @param manager the In-Band Bytestream manager
+ */
+ protected InitiationListener(InBandBytestreamManager manager) {
+ this.manager = manager;
+ initiationListenerExecutor = Executors.newCachedThreadPool();
+ }
+
+ public void processPacket(final Packet packet) {
+ initiationListenerExecutor.execute(new Runnable() {
+
+ public void run() {
+ processRequest(packet);
+ }
+ });
+ }
+
+ private void processRequest(Packet packet) {
+ Open ibbRequest = (Open) packet;
+
+ // validate that block size is within allowed range
+ if (ibbRequest.getBlockSize() > this.manager.getMaximumBlockSize()) {
+ this.manager.replyResourceConstraintPacket(ibbRequest);
+ return;
+ }
+
+ // ignore request if in ignore list
+ if (this.manager.getIgnoredBytestreamRequests().remove(ibbRequest.getSessionID()))
+ return;
+
+ // build bytestream request from packet
+ InBandBytestreamRequest request = new InBandBytestreamRequest(this.manager, ibbRequest);
+
+ // notify listeners for bytestream initiation from a specific user
+ BytestreamListener userListener = this.manager.getUserListener(ibbRequest.getFrom());
+ if (userListener != null) {
+ userListener.incomingBytestreamRequest(request);
+
+ }
+ else if (!this.manager.getAllRequestListeners().isEmpty()) {
+ /*
+ * if there is no user specific listener inform listeners for all initiation requests
+ */
+ for (BytestreamListener listener : this.manager.getAllRequestListeners()) {
+ listener.incomingBytestreamRequest(request);
+ }
+
+ }
+ else {
+ /*
+ * if there is no listener for this initiation request, reply with reject message
+ */
+ this.manager.replyRejectPacket(ibbRequest);
+ }
+ }
+
+ /**
+ * Returns the packet filter for In-Band Bytestream open requests.
+ *
+ * @return the packet filter for In-Band Bytestream open requests
+ */
+ protected PacketFilter getFilter() {
+ return this.initFilter;
+ }
+
+ /**
+ * Shuts down the listeners executor service.
+ */
+ protected void shutdown() {
+ this.initiationListenerExecutor.shutdownNow();
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Close.java b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Close.java new file mode 100644 index 0000000..9a78d73 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Close.java @@ -0,0 +1,65 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.ibb.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
+
+/**
+ * Represents a request to close an In-Band Bytestream.
+ *
+ * @author Henning Staib
+ */
+public class Close extends IQ {
+
+ /* unique session ID identifying this In-Band Bytestream */
+ private final String sessionID;
+
+ /**
+ * Creates a new In-Band Bytestream close request packet.
+ *
+ * @param sessionID unique session ID identifying this In-Band Bytestream
+ */
+ public Close(String sessionID) {
+ if (sessionID == null || "".equals(sessionID)) {
+ throw new IllegalArgumentException("Session ID must not be null or empty");
+ }
+ this.sessionID = sessionID;
+ setType(Type.SET);
+ }
+
+ /**
+ * Returns the unique session ID identifying this In-Band Bytestream.
+ *
+ * @return the unique session ID identifying this In-Band Bytestream
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ @Override
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<close ");
+ buf.append("xmlns=\"");
+ buf.append(InBandBytestreamManager.NAMESPACE);
+ buf.append("\" ");
+ buf.append("sid=\"");
+ buf.append(sessionID);
+ buf.append("\"");
+ buf.append("/>");
+ return buf.toString();
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Data.java b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Data.java new file mode 100644 index 0000000..696fa75 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Data.java @@ -0,0 +1,64 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.ibb.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+
+/**
+ * Represents a chunk of data sent over an In-Band Bytestream encapsulated in an
+ * IQ stanza.
+ *
+ * @author Henning Staib
+ */
+public class Data extends IQ {
+
+ /* the data packet extension */
+ private final DataPacketExtension dataPacketExtension;
+
+ /**
+ * Constructor.
+ *
+ * @param data data packet extension containing the encoded data
+ */
+ public Data(DataPacketExtension data) {
+ if (data == null) {
+ throw new IllegalArgumentException("Data must not be null");
+ }
+ this.dataPacketExtension = data;
+
+ /*
+ * also set as packet extension so that data packet extension can be
+ * retrieved from IQ stanza and message stanza in the same way
+ */
+ addExtension(data);
+ setType(IQ.Type.SET);
+ }
+
+ /**
+ * Returns the data packet extension.
+ * <p>
+ * Convenience method for <code>packet.getExtension("data",
+ * "http://jabber.org/protocol/ibb")</code>.
+ *
+ * @return the data packet extension
+ */
+ public DataPacketExtension getDataPacketExtension() {
+ return this.dataPacketExtension;
+ }
+
+ public String getChildElementXML() {
+ return this.dataPacketExtension.toXML();
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/packet/DataPacketExtension.java b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/DataPacketExtension.java new file mode 100644 index 0000000..80ed1e1 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/DataPacketExtension.java @@ -0,0 +1,149 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.ibb.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
+
+/**
+ * Represents a chunk of data of an In-Band Bytestream within an IQ stanza or a
+ * message stanza
+ *
+ * @author Henning Staib
+ */
+public class DataPacketExtension implements PacketExtension {
+
+ /**
+ * The element name of the data packet extension.
+ */
+ public final static String ELEMENT_NAME = "data";
+
+ /* unique session ID identifying this In-Band Bytestream */
+ private final String sessionID;
+
+ /* sequence of this packet in regard to the other data packets */
+ private final long seq;
+
+ /* the data contained in this packet */
+ private final String data;
+
+ private byte[] decodedData;
+
+ /**
+ * Creates a new In-Band Bytestream data packet.
+ *
+ * @param sessionID unique session ID identifying this In-Band Bytestream
+ * @param seq sequence of this packet in regard to the other data packets
+ * @param data the base64 encoded data contained in this packet
+ */
+ public DataPacketExtension(String sessionID, long seq, String data) {
+ if (sessionID == null || "".equals(sessionID)) {
+ throw new IllegalArgumentException("Session ID must not be null or empty");
+ }
+ if (seq < 0 || seq > 65535) {
+ throw new IllegalArgumentException("Sequence must not be between 0 and 65535");
+ }
+ if (data == null) {
+ throw new IllegalArgumentException("Data must not be null");
+ }
+ this.sessionID = sessionID;
+ this.seq = seq;
+ this.data = data;
+ }
+
+ /**
+ * Returns the unique session ID identifying this In-Band Bytestream.
+ *
+ * @return the unique session ID identifying this In-Band Bytestream
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ /**
+ * Returns the sequence of this packet in regard to the other data packets.
+ *
+ * @return the sequence of this packet in regard to the other data packets.
+ */
+ public long getSeq() {
+ return seq;
+ }
+
+ /**
+ * Returns the data contained in this packet.
+ *
+ * @return the data contained in this packet.
+ */
+ public String getData() {
+ return data;
+ }
+
+ /**
+ * Returns the decoded data or null if data could not be decoded.
+ * <p>
+ * The encoded data is invalid if it contains bad Base64 input characters or
+ * if it contains the pad ('=') character on a position other than the last
+ * character(s) of the data. See <a
+ * href="http://xmpp.org/extensions/xep-0047.html#sec">XEP-0047</a> Section
+ * 6.
+ *
+ * @return the decoded data
+ */
+ public byte[] getDecodedData() {
+ // return cached decoded data
+ if (this.decodedData != null) {
+ return this.decodedData;
+ }
+
+ // data must not contain the pad (=) other than end of data
+ if (data.matches(".*={1,2}+.+")) {
+ return null;
+ }
+
+ // decodeBase64 will return null if bad characters are included
+ this.decodedData = StringUtils.decodeBase64(data);
+ return this.decodedData;
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return InBandBytestreamManager.NAMESPACE;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<");
+ buf.append(getElementName());
+ buf.append(" ");
+ buf.append("xmlns=\"");
+ buf.append(InBandBytestreamManager.NAMESPACE);
+ buf.append("\" ");
+ buf.append("seq=\"");
+ buf.append(seq);
+ buf.append("\" ");
+ buf.append("sid=\"");
+ buf.append(sessionID);
+ buf.append("\">");
+ buf.append(data);
+ buf.append("</");
+ buf.append(getElementName());
+ buf.append(">");
+ return buf.toString();
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java new file mode 100644 index 0000000..94a7a9b --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java @@ -0,0 +1,126 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.ibb.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.StanzaType;
+
+/**
+ * Represents a request to open an In-Band Bytestream.
+ *
+ * @author Henning Staib
+ */
+public class Open extends IQ {
+
+ /* unique session ID identifying this In-Band Bytestream */
+ private final String sessionID;
+
+ /* block size in which the data will be fragmented */
+ private final int blockSize;
+
+ /* stanza type used to encapsulate the data */
+ private final StanzaType stanza;
+
+ /**
+ * Creates a new In-Band Bytestream open request packet.
+ * <p>
+ * The data sent over this In-Band Bytestream will be fragmented in blocks
+ * with the given block size. The block size should not be greater than
+ * 65535. A recommended default value is 4096.
+ * <p>
+ * The data can be sent using IQ stanzas or message stanzas.
+ *
+ * @param sessionID unique session ID identifying this In-Band Bytestream
+ * @param blockSize block size in which the data will be fragmented
+ * @param stanza stanza type used to encapsulate the data
+ */
+ public Open(String sessionID, int blockSize, StanzaType stanza) {
+ if (sessionID == null || "".equals(sessionID)) {
+ throw new IllegalArgumentException("Session ID must not be null or empty");
+ }
+ if (blockSize <= 0) {
+ throw new IllegalArgumentException("Block size must be greater than zero");
+ }
+
+ this.sessionID = sessionID;
+ this.blockSize = blockSize;
+ this.stanza = stanza;
+ setType(Type.SET);
+ }
+
+ /**
+ * Creates a new In-Band Bytestream open request packet.
+ * <p>
+ * The data sent over this In-Band Bytestream will be fragmented in blocks
+ * with the given block size. The block size should not be greater than
+ * 65535. A recommended default value is 4096.
+ * <p>
+ * The data will be sent using IQ stanzas.
+ *
+ * @param sessionID unique session ID identifying this In-Band Bytestream
+ * @param blockSize block size in which the data will be fragmented
+ */
+ public Open(String sessionID, int blockSize) {
+ this(sessionID, blockSize, StanzaType.IQ);
+ }
+
+ /**
+ * Returns the unique session ID identifying this In-Band Bytestream.
+ *
+ * @return the unique session ID identifying this In-Band Bytestream
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ /**
+ * Returns the block size in which the data will be fragmented.
+ *
+ * @return the block size in which the data will be fragmented
+ */
+ public int getBlockSize() {
+ return blockSize;
+ }
+
+ /**
+ * Returns the stanza type used to encapsulate the data.
+ *
+ * @return the stanza type used to encapsulate the data
+ */
+ public StanzaType getStanza() {
+ return stanza;
+ }
+
+ @Override
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<open ");
+ buf.append("xmlns=\"");
+ buf.append(InBandBytestreamManager.NAMESPACE);
+ buf.append("\" ");
+ buf.append("block-size=\"");
+ buf.append(blockSize);
+ buf.append("\" ");
+ buf.append("sid=\"");
+ buf.append(sessionID);
+ buf.append("\" ");
+ buf.append("stanza=\"");
+ buf.append(stanza.toString().toLowerCase());
+ buf.append("\"");
+ buf.append("/>");
+ return buf.toString();
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/provider/CloseIQProvider.java b/src/org/jivesoftware/smackx/bytestreams/ibb/provider/CloseIQProvider.java new file mode 100644 index 0000000..566724c --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/provider/CloseIQProvider.java @@ -0,0 +1,33 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.ibb.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Parses a close In-Band Bytestream packet.
+ *
+ * @author Henning Staib
+ */
+public class CloseIQProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ String sid = parser.getAttributeValue("", "sid");
+ return new Close(sid);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/provider/DataPacketProvider.java b/src/org/jivesoftware/smackx/bytestreams/ibb/provider/DataPacketProvider.java new file mode 100644 index 0000000..5abed08 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/provider/DataPacketProvider.java @@ -0,0 +1,45 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.ibb.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.Data;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Parses an In-Band Bytestream data packet which can be a packet extension of
+ * either an IQ stanza or a message stanza.
+ *
+ * @author Henning Staib
+ */
+public class DataPacketProvider implements PacketExtensionProvider, IQProvider {
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ String sessionID = parser.getAttributeValue("", "sid");
+ long seq = Long.parseLong(parser.getAttributeValue("", "seq"));
+ String data = parser.nextText();
+ return new DataPacketExtension(sessionID, seq, data);
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ DataPacketExtension data = (DataPacketExtension) parseExtension(parser);
+ IQ iq = new Data(data);
+ return iq;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/provider/OpenIQProvider.java b/src/org/jivesoftware/smackx/bytestreams/ibb/provider/OpenIQProvider.java new file mode 100644 index 0000000..3cc725a --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/ibb/provider/OpenIQProvider.java @@ -0,0 +1,45 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.ibb.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.StanzaType;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Parses an In-Band Bytestream open packet.
+ *
+ * @author Henning Staib
+ */
+public class OpenIQProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ String sessionID = parser.getAttributeValue("", "sid");
+ int blockSize = Integer.parseInt(parser.getAttributeValue("", "block-size"));
+
+ String stanzaValue = parser.getAttributeValue("", "stanza");
+ StanzaType stanza = null;
+ if (stanzaValue == null) {
+ stanza = StanzaType.IQ;
+ }
+ else {
+ stanza = StanzaType.valueOf(stanzaValue.toUpperCase());
+ }
+
+ return new Open(sessionID, blockSize, stanza);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/InitiationListener.java b/src/org/jivesoftware/smackx/bytestreams/socks5/InitiationListener.java new file mode 100644 index 0000000..2a78250 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/socks5/InitiationListener.java @@ -0,0 +1,119 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.socks5;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.IQTypeFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.bytestreams.BytestreamListener;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
+
+/**
+ * InitiationListener handles all incoming SOCKS5 Bytestream initiation requests. If there are no
+ * listeners for a SOCKS5 bytestream request InitiationListener will always refuse the request and
+ * reply with a <not-acceptable/> error (<a
+ * href="http://xmpp.org/extensions/xep-0065.html#usecase-alternate">XEP-0065</a> Section 5.2.A2).
+ *
+ * @author Henning Staib
+ */
+final class InitiationListener implements PacketListener {
+
+ /* manager containing the listeners and the XMPP connection */
+ private final Socks5BytestreamManager manager;
+
+ /* packet filter for all SOCKS5 Bytestream requests */
+ private final PacketFilter initFilter = new AndFilter(new PacketTypeFilter(Bytestream.class),
+ new IQTypeFilter(IQ.Type.SET));
+
+ /* executor service to process incoming requests concurrently */
+ private final ExecutorService initiationListenerExecutor;
+
+ /**
+ * Constructor
+ *
+ * @param manager the SOCKS5 Bytestream manager
+ */
+ protected InitiationListener(Socks5BytestreamManager manager) {
+ this.manager = manager;
+ initiationListenerExecutor = Executors.newCachedThreadPool();
+ }
+
+ public void processPacket(final Packet packet) {
+ initiationListenerExecutor.execute(new Runnable() {
+
+ public void run() {
+ processRequest(packet);
+ }
+ });
+ }
+
+ private void processRequest(Packet packet) {
+ Bytestream byteStreamRequest = (Bytestream) packet;
+
+ // ignore request if in ignore list
+ if (this.manager.getIgnoredBytestreamRequests().remove(byteStreamRequest.getSessionID())) {
+ return;
+ }
+
+ // build bytestream request from packet
+ Socks5BytestreamRequest request = new Socks5BytestreamRequest(this.manager,
+ byteStreamRequest);
+
+ // notify listeners for bytestream initiation from a specific user
+ BytestreamListener userListener = this.manager.getUserListener(byteStreamRequest.getFrom());
+ if (userListener != null) {
+ userListener.incomingBytestreamRequest(request);
+
+ }
+ else if (!this.manager.getAllRequestListeners().isEmpty()) {
+ /*
+ * if there is no user specific listener inform listeners for all initiation requests
+ */
+ for (BytestreamListener listener : this.manager.getAllRequestListeners()) {
+ listener.incomingBytestreamRequest(request);
+ }
+
+ }
+ else {
+ /*
+ * if there is no listener for this initiation request, reply with reject message
+ */
+ this.manager.replyRejectPacket(byteStreamRequest);
+ }
+ }
+
+ /**
+ * Returns the packet filter for SOCKS5 Bytestream initialization requests.
+ *
+ * @return the packet filter for SOCKS5 Bytestream initialization requests
+ */
+ protected PacketFilter getFilter() {
+ return this.initFilter;
+ }
+
+ /**
+ * Shuts down the listeners executor service.
+ */
+ protected void shutdown() {
+ this.initiationListenerExecutor.shutdownNow();
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamListener.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamListener.java new file mode 100644 index 0000000..1430b1d --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamListener.java @@ -0,0 +1,43 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.socks5;
+
+import org.jivesoftware.smackx.bytestreams.BytestreamListener;
+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
+
+/**
+ * Socks5BytestreamListener are informed if a remote user wants to initiate a SOCKS5 Bytestream.
+ * Implement this interface to handle incoming SOCKS5 Bytestream requests.
+ * <p>
+ * There are two ways to add this listener. See
+ * {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and
+ * {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for
+ * further details.
+ *
+ * @author Henning Staib
+ */
+public abstract class Socks5BytestreamListener implements BytestreamListener {
+
+ public void incomingBytestreamRequest(BytestreamRequest request) {
+ incomingBytestreamRequest((Socks5BytestreamRequest) request);
+ }
+
+ /**
+ * This listener is notified if a SOCKS5 Bytestream request from another user has been received.
+ *
+ * @param request the incoming SOCKS5 Bytestream request
+ */
+ public abstract void incomingBytestreamRequest(Socks5BytestreamRequest request);
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java new file mode 100644 index 0000000..1383495 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java @@ -0,0 +1,777 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.socks5;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeoutException;
+
+import org.jivesoftware.smack.AbstractConnectionListener;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.SyncPacketSend;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.bytestreams.BytestreamListener;
+import org.jivesoftware.smackx.bytestreams.BytestreamManager;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHostUsed;
+import org.jivesoftware.smackx.filetransfer.FileTransferManager;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
+import org.jivesoftware.smackx.packet.DiscoverItems.Item;
+
+/**
+ * The Socks5BytestreamManager class handles establishing SOCKS5 Bytestreams as specified in the <a
+ * href="http://xmpp.org/extensions/xep-0065.html">XEP-0065</a>.
+ * <p>
+ * A SOCKS5 Bytestream is negotiated partly over the XMPP XML stream and partly over a separate
+ * socket. The actual transfer though takes place over a separately created socket.
+ * <p>
+ * A SOCKS5 Bytestream generally has three parties, the initiator, the target, and the stream host.
+ * The stream host is a specialized SOCKS5 proxy setup on a server, or, the initiator can act as the
+ * stream host.
+ * <p>
+ * To establish a SOCKS5 Bytestream invoke the {@link #establishSession(String)} method. This will
+ * negotiate a SOCKS5 Bytestream with the given target JID and return a socket.
+ * <p>
+ * If a session ID for the SOCKS5 Bytestream was already negotiated (e.g. while negotiating a file
+ * transfer) invoke {@link #establishSession(String, String)}.
+ * <p>
+ * To handle incoming SOCKS5 Bytestream requests add an {@link Socks5BytestreamListener} to the
+ * manager. There are two ways to add this listener. If you want to be informed about incoming
+ * SOCKS5 Bytestreams from a specific user add the listener by invoking
+ * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should
+ * respond to all SOCKS5 Bytestream requests invoke
+ * {@link #addIncomingBytestreamListener(BytestreamListener)}.
+ * <p>
+ * Note that the registered {@link Socks5BytestreamListener} will NOT be notified on incoming Socks5
+ * bytestream requests sent in the context of <a
+ * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
+ * {@link FileTransferManager})
+ * <p>
+ * If no {@link Socks5BytestreamListener}s are registered, all incoming SOCKS5 Bytestream requests
+ * will be rejected by returning a <not-acceptable/> error to the initiator.
+ *
+ * @author Henning Staib
+ */
+public final class Socks5BytestreamManager implements BytestreamManager {
+
+ /*
+ * create a new Socks5BytestreamManager and register a shutdown listener on every established
+ * connection
+ */
+ static {
+ Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+
+ public void connectionCreated(final Connection connection) {
+ final Socks5BytestreamManager manager;
+ manager = Socks5BytestreamManager.getBytestreamManager(connection);
+
+ // register shutdown listener
+ connection.addConnectionListener(new AbstractConnectionListener() {
+
+ public void connectionClosed() {
+ manager.disableService();
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ manager.disableService();
+ }
+
+ public void reconnectionSuccessful() {
+ managers.put(connection, manager);
+ }
+
+ });
+ }
+
+ });
+ }
+
+ /**
+ * The XMPP namespace of the SOCKS5 Bytestream
+ */
+ public static final String NAMESPACE = "http://jabber.org/protocol/bytestreams";
+
+ /* prefix used to generate session IDs */
+ private static final String SESSION_ID_PREFIX = "js5_";
+
+ /* random generator to create session IDs */
+ private final static Random randomGenerator = new Random();
+
+ /* stores one Socks5BytestreamManager for each XMPP connection */
+ private final static Map<Connection, Socks5BytestreamManager> managers = new WeakHashMap<Connection, Socks5BytestreamManager>();
+
+ /* XMPP connection */
+ private final Connection connection;
+
+ /*
+ * assigns a user to a listener that is informed if a bytestream request for this user is
+ * received
+ */
+ private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();
+
+ /*
+ * list of listeners that respond to all bytestream requests if there are not user specific
+ * listeners for that request
+ */
+ private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());
+
+ /* listener that handles all incoming bytestream requests */
+ private final InitiationListener initiationListener;
+
+ /* timeout to wait for the response to the SOCKS5 Bytestream initialization request */
+ private int targetResponseTimeout = 10000;
+
+ /* timeout for connecting to the SOCKS5 proxy selected by the target */
+ private int proxyConnectionTimeout = 10000;
+
+ /* blacklist of errornous SOCKS5 proxies */
+ private final List<String> proxyBlacklist = Collections.synchronizedList(new LinkedList<String>());
+
+ /* remember the last proxy that worked to prioritize it */
+ private String lastWorkingProxy = null;
+
+ /* flag to enable/disable prioritization of last working proxy */
+ private boolean proxyPrioritizationEnabled = true;
+
+ /*
+ * list containing session IDs of SOCKS5 Bytestream initialization packets that should be
+ * ignored by the InitiationListener
+ */
+ private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());
+
+ /**
+ * Returns the Socks5BytestreamManager to handle SOCKS5 Bytestreams for a given
+ * {@link Connection}.
+ * <p>
+ * If no manager exists a new is created and initialized.
+ *
+ * @param connection the XMPP connection or <code>null</code> if given connection is
+ * <code>null</code>
+ * @return the Socks5BytestreamManager for the given XMPP connection
+ */
+ public static synchronized Socks5BytestreamManager getBytestreamManager(Connection connection) {
+ if (connection == null) {
+ return null;
+ }
+ Socks5BytestreamManager manager = managers.get(connection);
+ if (manager == null) {
+ manager = new Socks5BytestreamManager(connection);
+ managers.put(connection, manager);
+ manager.activate();
+ }
+ return manager;
+ }
+
+ /**
+ * Private constructor.
+ *
+ * @param connection the XMPP connection
+ */
+ private Socks5BytestreamManager(Connection connection) {
+ this.connection = connection;
+ this.initiationListener = new InitiationListener(this);
+ }
+
+ /**
+ * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request unless
+ * there is a user specific BytestreamListener registered.
+ * <p>
+ * If no listeners are registered all SOCKS5 Bytestream request are rejected with a
+ * <not-acceptable/> error.
+ * <p>
+ * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
+ * bytestream requests sent in the context of <a
+ * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
+ * {@link FileTransferManager})
+ *
+ * @param listener the listener to register
+ */
+ public void addIncomingBytestreamListener(BytestreamListener listener) {
+ this.allRequestListeners.add(listener);
+ }
+
+ /**
+ * Removes the given listener from the list of listeners for all incoming SOCKS5 Bytestream
+ * requests.
+ *
+ * @param listener the listener to remove
+ */
+ public void removeIncomingBytestreamListener(BytestreamListener listener) {
+ this.allRequestListeners.remove(listener);
+ }
+
+ /**
+ * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request from the
+ * given user.
+ * <p>
+ * Use this method if you are awaiting an incoming SOCKS5 Bytestream request from a specific
+ * user.
+ * <p>
+ * If no listeners are registered all SOCKS5 Bytestream request are rejected with a
+ * <not-acceptable/> error.
+ * <p>
+ * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5
+ * bytestream requests sent in the context of <a
+ * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See
+ * {@link FileTransferManager})
+ *
+ * @param listener the listener to register
+ * @param initiatorJID the JID of the user that wants to establish a SOCKS5 Bytestream
+ */
+ public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {
+ this.userListeners.put(initiatorJID, listener);
+ }
+
+ /**
+ * Removes the listener for the given user.
+ *
+ * @param initiatorJID the JID of the user the listener should be removed
+ */
+ public void removeIncomingBytestreamListener(String initiatorJID) {
+ this.userListeners.remove(initiatorJID);
+ }
+
+ /**
+ * Use this method to ignore the next incoming SOCKS5 Bytestream request containing the given
+ * session ID. No listeners will be notified for this request and and no error will be returned
+ * to the initiator.
+ * <p>
+ * This method should be used if you are awaiting a SOCKS5 Bytestream request as a reply to
+ * another packet (e.g. file transfer).
+ *
+ * @param sessionID to be ignored
+ */
+ public void ignoreBytestreamRequestOnce(String sessionID) {
+ this.ignoredBytestreamRequests.add(sessionID);
+ }
+
+ /**
+ * Disables the SOCKS5 Bytestream manager by removing the SOCKS5 Bytestream feature from the
+ * service discovery, disabling the listener for SOCKS5 Bytestream initiation requests and
+ * resetting its internal state.
+ * <p>
+ * To re-enable the SOCKS5 Bytestream feature invoke {@link #getBytestreamManager(Connection)}.
+ * Using the file transfer API will automatically re-enable the SOCKS5 Bytestream feature.
+ */
+ public synchronized void disableService() {
+
+ // remove initiation packet listener
+ this.connection.removePacketListener(this.initiationListener);
+
+ // shutdown threads
+ this.initiationListener.shutdown();
+
+ // clear listeners
+ this.allRequestListeners.clear();
+ this.userListeners.clear();
+
+ // reset internal state
+ this.lastWorkingProxy = null;
+ this.proxyBlacklist.clear();
+ this.ignoredBytestreamRequests.clear();
+
+ // remove manager from static managers map
+ managers.remove(this.connection);
+
+ // shutdown local SOCKS5 proxy if there are no more managers for other connections
+ if (managers.size() == 0) {
+ Socks5Proxy.getSocks5Proxy().stop();
+ }
+
+ // remove feature from service discovery
+ ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
+
+ // check if service discovery is not already disposed by connection shutdown
+ if (serviceDiscoveryManager != null) {
+ serviceDiscoveryManager.removeFeature(NAMESPACE);
+ }
+
+ }
+
+ /**
+ * Returns the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
+ * Default is 10000ms.
+ *
+ * @return the timeout to wait for the response to the SOCKS5 Bytestream initialization request
+ */
+ public int getTargetResponseTimeout() {
+ if (this.targetResponseTimeout <= 0) {
+ this.targetResponseTimeout = 10000;
+ }
+ return targetResponseTimeout;
+ }
+
+ /**
+ * Sets the timeout to wait for the response to the SOCKS5 Bytestream initialization request.
+ * Default is 10000ms.
+ *
+ * @param targetResponseTimeout the timeout to set
+ */
+ public void setTargetResponseTimeout(int targetResponseTimeout) {
+ this.targetResponseTimeout = targetResponseTimeout;
+ }
+
+ /**
+ * Returns the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
+ * 10000ms.
+ *
+ * @return the timeout for connecting to the SOCKS5 proxy selected by the target
+ */
+ public int getProxyConnectionTimeout() {
+ if (this.proxyConnectionTimeout <= 0) {
+ this.proxyConnectionTimeout = 10000;
+ }
+ return proxyConnectionTimeout;
+ }
+
+ /**
+ * Sets the timeout for connecting to the SOCKS5 proxy selected by the target. Default is
+ * 10000ms.
+ *
+ * @param proxyConnectionTimeout the timeout to set
+ */
+ public void setProxyConnectionTimeout(int proxyConnectionTimeout) {
+ this.proxyConnectionTimeout = proxyConnectionTimeout;
+ }
+
+ /**
+ * Returns if the prioritization of the last working SOCKS5 proxy on successive SOCKS5
+ * Bytestream connections is enabled. Default is <code>true</code>.
+ *
+ * @return <code>true</code> if prioritization is enabled, <code>false</code> otherwise
+ */
+ public boolean isProxyPrioritizationEnabled() {
+ return proxyPrioritizationEnabled;
+ }
+
+ /**
+ * Enable/disable the prioritization of the last working SOCKS5 proxy on successive SOCKS5
+ * Bytestream connections.
+ *
+ * @param proxyPrioritizationEnabled enable/disable the prioritization of the last working
+ * SOCKS5 proxy
+ */
+ public void setProxyPrioritizationEnabled(boolean proxyPrioritizationEnabled) {
+ this.proxyPrioritizationEnabled = proxyPrioritizationEnabled;
+ }
+
+ /**
+ * Establishes a SOCKS5 Bytestream with the given user and returns the Socket to send/receive
+ * data to/from the user.
+ * <p>
+ * Use this method to establish SOCKS5 Bytestreams to users accepting all incoming Socks5
+ * bytestream requests since this method doesn't provide a way to tell the user something about
+ * the data to be sent.
+ * <p>
+ * To establish a SOCKS5 Bytestream after negotiation the kind of data to be sent (e.g. file
+ * transfer) use {@link #establishSession(String, String)}.
+ *
+ * @param targetJID the JID of the user a SOCKS5 Bytestream should be established
+ * @return the Socket to send/receive data to/from the user
+ * @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
+ * Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
+ * @throws IOException if the bytestream could not be established
+ * @throws InterruptedException if the current thread was interrupted while waiting
+ */
+ public Socks5BytestreamSession establishSession(String targetJID) throws XMPPException,
+ IOException, InterruptedException {
+ String sessionID = getNextSessionID();
+ return establishSession(targetJID, sessionID);
+ }
+
+ /**
+ * Establishes a SOCKS5 Bytestream with the given user using the given session ID and returns
+ * the Socket to send/receive data to/from the user.
+ *
+ * @param targetJID the JID of the user a SOCKS5 Bytestream should be established
+ * @param sessionID the session ID for the SOCKS5 Bytestream request
+ * @return the Socket to send/receive data to/from the user
+ * @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5
+ * Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies
+ * @throws IOException if the bytestream could not be established
+ * @throws InterruptedException if the current thread was interrupted while waiting
+ */
+ public Socks5BytestreamSession establishSession(String targetJID, String sessionID)
+ throws XMPPException, IOException, InterruptedException {
+
+ XMPPException discoveryException = null;
+ // check if target supports SOCKS5 Bytestream
+ if (!supportsSocks5(targetJID)) {
+ throw new XMPPException(targetJID + " doesn't support SOCKS5 Bytestream");
+ }
+
+ List<String> proxies = new ArrayList<String>();
+ // determine SOCKS5 proxies from XMPP-server
+ try {
+ proxies.addAll(determineProxies());
+ } catch (XMPPException e) {
+ // don't abort here, just remember the exception thrown by determineProxies()
+ // determineStreamHostInfos() will at least add the local Socks5 proxy (if enabled)
+ discoveryException = e;
+ }
+
+ // determine address and port of each proxy
+ List<StreamHost> streamHosts = determineStreamHostInfos(proxies);
+
+ if (streamHosts.isEmpty()) {
+ throw discoveryException != null ? discoveryException : new XMPPException("no SOCKS5 proxies available");
+ }
+
+ // compute digest
+ String digest = Socks5Utils.createDigest(sessionID, this.connection.getUser(), targetJID);
+
+ // prioritize last working SOCKS5 proxy if exists
+ if (this.proxyPrioritizationEnabled && this.lastWorkingProxy != null) {
+ StreamHost selectedStreamHost = null;
+ for (StreamHost streamHost : streamHosts) {
+ if (streamHost.getJID().equals(this.lastWorkingProxy)) {
+ selectedStreamHost = streamHost;
+ break;
+ }
+ }
+ if (selectedStreamHost != null) {
+ streamHosts.remove(selectedStreamHost);
+ streamHosts.add(0, selectedStreamHost);
+ }
+
+ }
+
+ Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();
+ try {
+
+ // add transfer digest to local proxy to make transfer valid
+ socks5Proxy.addTransfer(digest);
+
+ // create initiation packet
+ Bytestream initiation = createBytestreamInitiation(sessionID, targetJID, streamHosts);
+
+ // send initiation packet
+ Packet response = SyncPacketSend.getReply(this.connection, initiation,
+ getTargetResponseTimeout());
+
+ // extract used stream host from response
+ StreamHostUsed streamHostUsed = ((Bytestream) response).getUsedHost();
+ StreamHost usedStreamHost = initiation.getStreamHost(streamHostUsed.getJID());
+
+ if (usedStreamHost == null) {
+ throw new XMPPException("Remote user responded with unknown host");
+ }
+
+ // build SOCKS5 client
+ Socks5Client socks5Client = new Socks5ClientForInitiator(usedStreamHost, digest,
+ this.connection, sessionID, targetJID);
+
+ // establish connection to proxy
+ Socket socket = socks5Client.getSocket(getProxyConnectionTimeout());
+
+ // remember last working SOCKS5 proxy to prioritize it for next request
+ this.lastWorkingProxy = usedStreamHost.getJID();
+
+ // negotiation successful, return the output stream
+ return new Socks5BytestreamSession(socket, usedStreamHost.getJID().equals(
+ this.connection.getUser()));
+
+ }
+ catch (TimeoutException e) {
+ throw new IOException("Timeout while connecting to SOCKS5 proxy");
+ }
+ finally {
+
+ // remove transfer digest if output stream is returned or an exception
+ // occurred
+ socks5Proxy.removeTransfer(digest);
+
+ }
+ }
+
+ /**
+ * Returns <code>true</code> if the given target JID supports feature SOCKS5 Bytestream.
+ *
+ * @param targetJID the target JID
+ * @return <code>true</code> if the given target JID supports feature SOCKS5 Bytestream
+ * otherwise <code>false</code>
+ * @throws XMPPException if there was an error querying target for supported features
+ */
+ private boolean supportsSocks5(String targetJID) throws XMPPException {
+ ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
+ DiscoverInfo discoverInfo = serviceDiscoveryManager.discoverInfo(targetJID);
+ return discoverInfo.containsFeature(NAMESPACE);
+ }
+
+ /**
+ * Returns a list of JIDs of SOCKS5 proxies by querying the XMPP server. The SOCKS5 proxies are
+ * in the same order as returned by the XMPP server.
+ *
+ * @return list of JIDs of SOCKS5 proxies
+ * @throws XMPPException if there was an error querying the XMPP server for SOCKS5 proxies
+ */
+ private List<String> determineProxies() throws XMPPException {
+ ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);
+
+ List<String> proxies = new ArrayList<String>();
+
+ // get all items form XMPP server
+ DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(this.connection.getServiceName());
+ Iterator<Item> itemIterator = discoverItems.getItems();
+
+ // query all items if they are SOCKS5 proxies
+ while (itemIterator.hasNext()) {
+ Item item = itemIterator.next();
+
+ // skip blacklisted servers
+ if (this.proxyBlacklist.contains(item.getEntityID())) {
+ continue;
+ }
+
+ try {
+ DiscoverInfo proxyInfo;
+ proxyInfo = serviceDiscoveryManager.discoverInfo(item.getEntityID());
+ Iterator<Identity> identities = proxyInfo.getIdentities();
+
+ // item must have category "proxy" and type "bytestream"
+ while (identities.hasNext()) {
+ Identity identity = identities.next();
+
+ if ("proxy".equalsIgnoreCase(identity.getCategory())
+ && "bytestreams".equalsIgnoreCase(identity.getType())) {
+ proxies.add(item.getEntityID());
+ break;
+ }
+
+ /*
+ * server is not a SOCKS5 proxy, blacklist server to skip next time a Socks5
+ * bytestream should be established
+ */
+ this.proxyBlacklist.add(item.getEntityID());
+
+ }
+ }
+ catch (XMPPException e) {
+ // blacklist errornous server
+ this.proxyBlacklist.add(item.getEntityID());
+ }
+ }
+
+ return proxies;
+ }
+
+ /**
+ * Returns a list of stream hosts containing the IP address an the port for the given list of
+ * SOCKS5 proxy JIDs. The order of the returned list is the same as the given list of JIDs
+ * excluding all SOCKS5 proxies who's network settings could not be determined. If a local
+ * SOCKS5 proxy is running it will be the first item in the list returned.
+ *
+ * @param proxies a list of SOCKS5 proxy JIDs
+ * @return a list of stream hosts containing the IP address an the port
+ */
+ private List<StreamHost> determineStreamHostInfos(List<String> proxies) {
+ List<StreamHost> streamHosts = new ArrayList<StreamHost>();
+
+ // add local proxy on first position if exists
+ List<StreamHost> localProxies = getLocalStreamHost();
+ if (localProxies != null) {
+ streamHosts.addAll(localProxies);
+ }
+
+ // query SOCKS5 proxies for network settings
+ for (String proxy : proxies) {
+ Bytestream streamHostRequest = createStreamHostRequest(proxy);
+ try {
+ Bytestream response = (Bytestream) SyncPacketSend.getReply(this.connection,
+ streamHostRequest);
+ streamHosts.addAll(response.getStreamHosts());
+ }
+ catch (XMPPException e) {
+ // blacklist errornous proxies
+ this.proxyBlacklist.add(proxy);
+ }
+ }
+
+ return streamHosts;
+ }
+
+ /**
+ * Returns a IQ packet to query a SOCKS5 proxy its network settings.
+ *
+ * @param proxy the proxy to query
+ * @return IQ packet to query a SOCKS5 proxy its network settings
+ */
+ private Bytestream createStreamHostRequest(String proxy) {
+ Bytestream request = new Bytestream();
+ request.setType(IQ.Type.GET);
+ request.setTo(proxy);
+ return request;
+ }
+
+ /**
+ * Returns the stream host information of the local SOCKS5 proxy containing the IP address and
+ * the port or null if local SOCKS5 proxy is not running.
+ *
+ * @return the stream host information of the local SOCKS5 proxy or null if local SOCKS5 proxy
+ * is not running
+ */
+ private List<StreamHost> getLocalStreamHost() {
+
+ // get local proxy singleton
+ Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();
+
+ if (socks5Server.isRunning()) {
+ List<String> addresses = socks5Server.getLocalAddresses();
+ int port = socks5Server.getPort();
+
+ if (addresses.size() >= 1) {
+ List<StreamHost> streamHosts = new ArrayList<StreamHost>();
+ for (String address : addresses) {
+ StreamHost streamHost = new StreamHost(this.connection.getUser(), address);
+ streamHost.setPort(port);
+ streamHosts.add(streamHost);
+ }
+ return streamHosts;
+ }
+
+ }
+
+ // server is not running or local address could not be determined
+ return null;
+ }
+
+ /**
+ * Returns a SOCKS5 Bytestream initialization request packet with the given session ID
+ * containing the given stream hosts for the given target JID.
+ *
+ * @param sessionID the session ID for the SOCKS5 Bytestream
+ * @param targetJID the target JID of SOCKS5 Bytestream request
+ * @param streamHosts a list of SOCKS5 proxies the target should connect to
+ * @return a SOCKS5 Bytestream initialization request packet
+ */
+ private Bytestream createBytestreamInitiation(String sessionID, String targetJID,
+ List<StreamHost> streamHosts) {
+ Bytestream initiation = new Bytestream(sessionID);
+
+ // add all stream hosts
+ for (StreamHost streamHost : streamHosts) {
+ initiation.addStreamHost(streamHost);
+ }
+
+ initiation.setType(IQ.Type.SET);
+ initiation.setTo(targetJID);
+
+ return initiation;
+ }
+
+ /**
+ * Responses to the given packet's sender with a XMPP error that a SOCKS5 Bytestream is not
+ * accepted.
+ *
+ * @param packet Packet that should be answered with a not-acceptable error
+ */
+ protected void replyRejectPacket(IQ packet) {
+ XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);
+ IQ errorIQ = IQ.createErrorResponse(packet, xmppError);
+ this.connection.sendPacket(errorIQ);
+ }
+
+ /**
+ * Activates the Socks5BytestreamManager by registering the SOCKS5 Bytestream initialization
+ * listener and enabling the SOCKS5 Bytestream feature.
+ */
+ private void activate() {
+ // register bytestream initiation packet listener
+ this.connection.addPacketListener(this.initiationListener,
+ this.initiationListener.getFilter());
+
+ // enable SOCKS5 feature
+ enableService();
+ }
+
+ /**
+ * Adds the SOCKS5 Bytestream feature to the service discovery.
+ */
+ private void enableService() {
+ ServiceDiscoveryManager manager = ServiceDiscoveryManager.getInstanceFor(this.connection);
+ if (!manager.includesFeature(NAMESPACE)) {
+ manager.addFeature(NAMESPACE);
+ }
+ }
+
+ /**
+ * Returns a new unique session ID.
+ *
+ * @return a new unique session ID
+ */
+ private String getNextSessionID() {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append(SESSION_ID_PREFIX);
+ buffer.append(Math.abs(randomGenerator.nextLong()));
+ return buffer.toString();
+ }
+
+ /**
+ * Returns the XMPP connection.
+ *
+ * @return the XMPP connection
+ */
+ protected Connection getConnection() {
+ return this.connection;
+ }
+
+ /**
+ * Returns the {@link BytestreamListener} that should be informed if a SOCKS5 Bytestream request
+ * from the given initiator JID is received.
+ *
+ * @param initiator the initiator's JID
+ * @return the listener
+ */
+ protected BytestreamListener getUserListener(String initiator) {
+ return this.userListeners.get(initiator);
+ }
+
+ /**
+ * Returns a list of {@link BytestreamListener} that are informed if there are no listeners for
+ * a specific initiator.
+ *
+ * @return list of listeners
+ */
+ protected List<BytestreamListener> getAllRequestListeners() {
+ return this.allRequestListeners;
+ }
+
+ /**
+ * Returns the list of session IDs that should be ignored by the InitialtionListener
+ *
+ * @return list of session IDs
+ */
+ protected List<String> getIgnoredBytestreamRequests() {
+ return ignoredBytestreamRequests;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java new file mode 100644 index 0000000..0b2fdeb --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java @@ -0,0 +1,316 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.socks5;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.Collection;
+import java.util.concurrent.TimeoutException;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.Cache;
+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
+
+/**
+ * Socks5BytestreamRequest class handles incoming SOCKS5 Bytestream requests.
+ *
+ * @author Henning Staib
+ */
+public class Socks5BytestreamRequest implements BytestreamRequest {
+
+ /* lifetime of an Item in the blacklist */
+ private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120;
+
+ /* size of the blacklist */
+ private static final int BLACKLIST_MAX_SIZE = 100;
+
+ /* blacklist of addresses of SOCKS5 proxies */
+ private static final Cache<String, Integer> ADDRESS_BLACKLIST = new Cache<String, Integer>(
+ BLACKLIST_MAX_SIZE, BLACKLIST_LIFETIME);
+
+ /*
+ * The number of connection failures it takes for a particular SOCKS5 proxy to be blacklisted.
+ * When a proxy is blacklisted no more connection attempts will be made to it for a period of 2
+ * hours.
+ */
+ private static int CONNECTION_FAILURE_THRESHOLD = 2;
+
+ /* the bytestream initialization request */
+ private Bytestream bytestreamRequest;
+
+ /* SOCKS5 Bytestream manager containing the XMPP connection and helper methods */
+ private Socks5BytestreamManager manager;
+
+ /* timeout to connect to all SOCKS5 proxies */
+ private int totalConnectTimeout = 10000;
+
+ /* minimum timeout to connect to one SOCKS5 proxy */
+ private int minimumConnectTimeout = 2000;
+
+ /**
+ * Returns the number of connection failures it takes for a particular SOCKS5 proxy to be
+ * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
+ * period of 2 hours. Default is 2.
+ *
+ * @return the number of connection failures it takes for a particular SOCKS5 proxy to be
+ * blacklisted
+ */
+ public static int getConnectFailureThreshold() {
+ return CONNECTION_FAILURE_THRESHOLD;
+ }
+
+ /**
+ * Sets the number of connection failures it takes for a particular SOCKS5 proxy to be
+ * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a
+ * period of 2 hours. Default is 2.
+ * <p>
+ * Setting the connection failure threshold to zero disables the blacklisting.
+ *
+ * @param connectFailureThreshold the number of connection failures it takes for a particular
+ * SOCKS5 proxy to be blacklisted
+ */
+ public static void setConnectFailureThreshold(int connectFailureThreshold) {
+ CONNECTION_FAILURE_THRESHOLD = connectFailureThreshold;
+ }
+
+ /**
+ * Creates a new Socks5BytestreamRequest.
+ *
+ * @param manager the SOCKS5 Bytestream manager
+ * @param bytestreamRequest the SOCKS5 Bytestream initialization packet
+ */
+ protected Socks5BytestreamRequest(Socks5BytestreamManager manager, Bytestream bytestreamRequest) {
+ this.manager = manager;
+ this.bytestreamRequest = bytestreamRequest;
+ }
+
+ /**
+ * Returns the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
+ * <p>
+ * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
+ * by the initiator until a connection is established. This timeout divided by the number of
+ * SOCKS5 proxies determines the timeout for every connection attempt.
+ * <p>
+ * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
+ * {@link #setMinimumConnectTimeout(int)}.
+ *
+ * @return the maximum timeout to connect to SOCKS5 proxies
+ */
+ public int getTotalConnectTimeout() {
+ if (this.totalConnectTimeout <= 0) {
+ return 10000;
+ }
+ return this.totalConnectTimeout;
+ }
+
+ /**
+ * Sets the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.
+ * <p>
+ * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given
+ * by the initiator until a connection is established. This timeout divided by the number of
+ * SOCKS5 proxies determines the timeout for every connection attempt.
+ * <p>
+ * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking
+ * {@link #setMinimumConnectTimeout(int)}.
+ *
+ * @param totalConnectTimeout the maximum timeout to connect to SOCKS5 proxies
+ */
+ public void setTotalConnectTimeout(int totalConnectTimeout) {
+ this.totalConnectTimeout = totalConnectTimeout;
+ }
+
+ /**
+ * Returns the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
+ * request. Default is 2000ms.
+ *
+ * @return the timeout to connect to one SOCKS5 proxy
+ */
+ public int getMinimumConnectTimeout() {
+ if (this.minimumConnectTimeout <= 0) {
+ return 2000;
+ }
+ return this.minimumConnectTimeout;
+ }
+
+ /**
+ * Sets the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream
+ * request. Default is 2000ms.
+ *
+ * @param minimumConnectTimeout the timeout to connect to one SOCKS5 proxy
+ */
+ public void setMinimumConnectTimeout(int minimumConnectTimeout) {
+ this.minimumConnectTimeout = minimumConnectTimeout;
+ }
+
+ /**
+ * Returns the sender of the SOCKS5 Bytestream initialization request.
+ *
+ * @return the sender of the SOCKS5 Bytestream initialization request.
+ */
+ public String getFrom() {
+ return this.bytestreamRequest.getFrom();
+ }
+
+ /**
+ * Returns the session ID of the SOCKS5 Bytestream initialization request.
+ *
+ * @return the session ID of the SOCKS5 Bytestream initialization request.
+ */
+ public String getSessionID() {
+ return this.bytestreamRequest.getSessionID();
+ }
+
+ /**
+ * Accepts the SOCKS5 Bytestream initialization request and returns the socket to send/receive
+ * data.
+ * <p>
+ * Before accepting the SOCKS5 Bytestream request you can set timeouts by invoking
+ * {@link #setTotalConnectTimeout(int)} and {@link #setMinimumConnectTimeout(int)}.
+ *
+ * @return the socket to send/receive data
+ * @throws XMPPException if connection to all SOCKS5 proxies failed or if stream is invalid.
+ * @throws InterruptedException if the current thread was interrupted while waiting
+ */
+ public Socks5BytestreamSession accept() throws XMPPException, InterruptedException {
+ Collection<StreamHost> streamHosts = this.bytestreamRequest.getStreamHosts();
+
+ // throw exceptions if request contains no stream hosts
+ if (streamHosts.size() == 0) {
+ cancelRequest();
+ }
+
+ StreamHost selectedHost = null;
+ Socket socket = null;
+
+ String digest = Socks5Utils.createDigest(this.bytestreamRequest.getSessionID(),
+ this.bytestreamRequest.getFrom(), this.manager.getConnection().getUser());
+
+ /*
+ * determine timeout for each connection attempt; each SOCKS5 proxy has the same amount of
+ * time so that the first does not consume the whole timeout
+ */
+ int timeout = Math.max(getTotalConnectTimeout() / streamHosts.size(),
+ getMinimumConnectTimeout());
+
+ for (StreamHost streamHost : streamHosts) {
+ String address = streamHost.getAddress() + ":" + streamHost.getPort();
+
+ // check to see if this address has been blacklisted
+ int failures = getConnectionFailures(address);
+ if (CONNECTION_FAILURE_THRESHOLD > 0 && failures >= CONNECTION_FAILURE_THRESHOLD) {
+ continue;
+ }
+
+ // establish socket
+ try {
+
+ // build SOCKS5 client
+ final Socks5Client socks5Client = new Socks5Client(streamHost, digest);
+
+ // connect to SOCKS5 proxy with a timeout
+ socket = socks5Client.getSocket(timeout);
+
+ // set selected host
+ selectedHost = streamHost;
+ break;
+
+ }
+ catch (TimeoutException e) {
+ incrementConnectionFailures(address);
+ }
+ catch (IOException e) {
+ incrementConnectionFailures(address);
+ }
+ catch (XMPPException e) {
+ incrementConnectionFailures(address);
+ }
+
+ }
+
+ // throw exception if connecting to all SOCKS5 proxies failed
+ if (selectedHost == null || socket == null) {
+ cancelRequest();
+ }
+
+ // send used-host confirmation
+ Bytestream response = createUsedHostResponse(selectedHost);
+ this.manager.getConnection().sendPacket(response);
+
+ return new Socks5BytestreamSession(socket, selectedHost.getJID().equals(
+ this.bytestreamRequest.getFrom()));
+
+ }
+
+ /**
+ * Rejects the SOCKS5 Bytestream request by sending a reject error to the initiator.
+ */
+ public void reject() {
+ this.manager.replyRejectPacket(this.bytestreamRequest);
+ }
+
+ /**
+ * Cancels the SOCKS5 Bytestream request by sending an error to the initiator and building a
+ * XMPP exception.
+ *
+ * @throws XMPPException XMPP exception containing the XMPP error
+ */
+ private void cancelRequest() throws XMPPException {
+ String errorMessage = "Could not establish socket with any provided host";
+ XMPPError error = new XMPPError(XMPPError.Condition.item_not_found, errorMessage);
+ IQ errorIQ = IQ.createErrorResponse(this.bytestreamRequest, error);
+ this.manager.getConnection().sendPacket(errorIQ);
+ throw new XMPPException(errorMessage, error);
+ }
+
+ /**
+ * Returns the response to the SOCKS5 Bytestream request containing the SOCKS5 proxy used.
+ *
+ * @param selectedHost the used SOCKS5 proxy
+ * @return the response to the SOCKS5 Bytestream request
+ */
+ private Bytestream createUsedHostResponse(StreamHost selectedHost) {
+ Bytestream response = new Bytestream(this.bytestreamRequest.getSessionID());
+ response.setTo(this.bytestreamRequest.getFrom());
+ response.setType(IQ.Type.RESULT);
+ response.setPacketID(this.bytestreamRequest.getPacketID());
+ response.setUsedHost(selectedHost.getJID());
+ return response;
+ }
+
+ /**
+ * Increments the connection failure counter by one for the given address.
+ *
+ * @param address the address the connection failure counter should be increased
+ */
+ private void incrementConnectionFailures(String address) {
+ Integer count = ADDRESS_BLACKLIST.get(address);
+ ADDRESS_BLACKLIST.put(address, count == null ? 1 : count + 1);
+ }
+
+ /**
+ * Returns how often the connection to the given address failed.
+ *
+ * @param address the address
+ * @return number of connection failures
+ */
+ private int getConnectionFailures(String address) {
+ Integer count = ADDRESS_BLACKLIST.get(address);
+ return count != null ? count : 0;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java new file mode 100644 index 0000000..41ab142 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java @@ -0,0 +1,98 @@ +/** + * $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.smackx.bytestreams.socks5;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.SocketException;
+
+import org.jivesoftware.smackx.bytestreams.BytestreamSession;
+
+/**
+ * Socks5BytestreamSession class represents a SOCKS5 Bytestream session.
+ *
+ * @author Henning Staib
+ */
+public class Socks5BytestreamSession implements BytestreamSession {
+
+ /* the underlying socket of the SOCKS5 Bytestream */
+ private final Socket socket;
+
+ /* flag to indicate if this session is a direct or mediated connection */
+ private final boolean isDirect;
+
+ protected Socks5BytestreamSession(Socket socket, boolean isDirect) {
+ this.socket = socket;
+ this.isDirect = isDirect;
+ }
+
+ /**
+ * Returns <code>true</code> if the session is established through a direct connection between
+ * the initiator and target, <code>false</code> if the session is mediated over a SOCKS proxy.
+ *
+ * @return <code>true</code> if session is a direct connection, <code>false</code> if session is
+ * mediated over a SOCKS5 proxy
+ */
+ public boolean isDirect() {
+ return this.isDirect;
+ }
+
+ /**
+ * Returns <code>true</code> if the session is mediated over a SOCKS proxy, <code>false</code>
+ * if this session is established through a direct connection between the initiator and target.
+ *
+ * @return <code>true</code> if session is mediated over a SOCKS5 proxy, <code>false</code> if
+ * session is a direct connection
+ */
+ public boolean isMediated() {
+ return !this.isDirect;
+ }
+
+ public InputStream getInputStream() throws IOException {
+ return this.socket.getInputStream();
+ }
+
+ public OutputStream getOutputStream() throws IOException {
+ return this.socket.getOutputStream();
+ }
+
+ public int getReadTimeout() throws IOException {
+ try {
+ return this.socket.getSoTimeout();
+ }
+ catch (SocketException e) {
+ throw new IOException("Error on underlying Socket");
+ }
+ }
+
+ public void setReadTimeout(int timeout) throws IOException {
+ try {
+ this.socket.setSoTimeout(timeout);
+ }
+ catch (SocketException e) {
+ throw new IOException("Error on underlying Socket");
+ }
+ }
+
+ public void close() throws IOException {
+ this.socket.close();
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java new file mode 100644 index 0000000..664ea59 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java @@ -0,0 +1,204 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.socks5;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
+
+/**
+ * The SOCKS5 client class handles establishing a connection to a SOCKS5 proxy. Connecting to a
+ * SOCKS5 proxy requires authentication. This implementation only supports the no-authentication
+ * authentication method.
+ *
+ * @author Henning Staib
+ */
+class Socks5Client {
+
+ /* stream host containing network settings and name of the SOCKS5 proxy */
+ protected StreamHost streamHost;
+
+ /* SHA-1 digest identifying the SOCKS5 stream */
+ protected String digest;
+
+ /**
+ * Constructor for a SOCKS5 client.
+ *
+ * @param streamHost containing network settings of the SOCKS5 proxy
+ * @param digest identifying the SOCKS5 Bytestream
+ */
+ public Socks5Client(StreamHost streamHost, String digest) {
+ this.streamHost = streamHost;
+ this.digest = digest;
+ }
+
+ /**
+ * Returns the initialized socket that can be used to transfer data between peers via the SOCKS5
+ * proxy.
+ *
+ * @param timeout timeout to connect to SOCKS5 proxy in milliseconds
+ * @return socket the initialized socket
+ * @throws IOException if initializing the socket failed due to a network error
+ * @throws XMPPException if establishing connection to SOCKS5 proxy failed
+ * @throws TimeoutException if connecting to SOCKS5 proxy timed out
+ * @throws InterruptedException if the current thread was interrupted while waiting
+ */
+ public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException,
+ TimeoutException {
+
+ // wrap connecting in future for timeout
+ FutureTask<Socket> futureTask = new FutureTask<Socket>(new Callable<Socket>() {
+
+ public Socket call() throws Exception {
+
+ // initialize socket
+ Socket socket = new Socket();
+ SocketAddress socketAddress = new InetSocketAddress(streamHost.getAddress(),
+ streamHost.getPort());
+ socket.connect(socketAddress);
+
+ // initialize connection to SOCKS5 proxy
+ if (!establish(socket)) {
+
+ // initialization failed, close socket
+ socket.close();
+ throw new XMPPException("establishing connection to SOCKS5 proxy failed");
+
+ }
+
+ return socket;
+ }
+
+ });
+ Thread executor = new Thread(futureTask);
+ executor.start();
+
+ // get connection to initiator with timeout
+ try {
+ return futureTask.get(timeout, TimeUnit.MILLISECONDS);
+ }
+ catch (ExecutionException e) {
+ Throwable cause = e.getCause();
+ if (cause != null) {
+ // case exceptions to comply with method signature
+ if (cause instanceof IOException) {
+ throw (IOException) cause;
+ }
+ if (cause instanceof XMPPException) {
+ throw (XMPPException) cause;
+ }
+ }
+
+ // throw generic IO exception if unexpected exception was thrown
+ throw new IOException("Error while connection to SOCKS5 proxy");
+ }
+
+ }
+
+ /**
+ * Initializes the connection to the SOCKS5 proxy by negotiating authentication method and
+ * requesting a stream for the given digest. Currently only the no-authentication method is
+ * supported by the Socks5Client.
+ * <p>
+ * Returns <code>true</code> if a stream could be established, otherwise <code>false</code>. If
+ * <code>false</code> is returned the given Socket should be closed.
+ *
+ * @param socket connected to a SOCKS5 proxy
+ * @return <code>true</code> if if a stream could be established, otherwise <code>false</code>.
+ * If <code>false</code> is returned the given Socket should be closed.
+ * @throws IOException if a network error occurred
+ */
+ protected boolean establish(Socket socket) throws IOException {
+
+ /*
+ * use DataInputStream/DataOutpuStream to assure read and write is completed in a single
+ * statement
+ */
+ DataInputStream in = new DataInputStream(socket.getInputStream());
+ DataOutputStream out = new DataOutputStream(socket.getOutputStream());
+
+ // authentication negotiation
+ byte[] cmd = new byte[3];
+
+ cmd[0] = (byte) 0x05; // protocol version 5
+ cmd[1] = (byte) 0x01; // number of authentication methods supported
+ cmd[2] = (byte) 0x00; // authentication method: no-authentication required
+
+ out.write(cmd);
+ out.flush();
+
+ byte[] response = new byte[2];
+ in.readFully(response);
+
+ // check if server responded with correct version and no-authentication method
+ if (response[0] != (byte) 0x05 || response[1] != (byte) 0x00) {
+ return false;
+ }
+
+ // request SOCKS5 connection with given address/digest
+ byte[] connectionRequest = createSocks5ConnectRequest();
+ out.write(connectionRequest);
+ out.flush();
+
+ // receive response
+ byte[] connectionResponse;
+ try {
+ connectionResponse = Socks5Utils.receiveSocks5Message(in);
+ }
+ catch (XMPPException e) {
+ return false; // server answered in an unsupported way
+ }
+
+ // verify response
+ connectionRequest[1] = (byte) 0x00; // set expected return status to 0
+ return Arrays.equals(connectionRequest, connectionResponse);
+ }
+
+ /**
+ * Returns a SOCKS5 connection request message. It contains the command "connect", the address
+ * type "domain" and the digest as address.
+ * <p>
+ * (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)
+ *
+ * @return SOCKS5 connection request message
+ */
+ private byte[] createSocks5ConnectRequest() {
+ byte addr[] = this.digest.getBytes();
+
+ byte[] data = new byte[7 + addr.length];
+ data[0] = (byte) 0x05; // version (SOCKS5)
+ data[1] = (byte) 0x01; // command (1 - connect)
+ data[2] = (byte) 0x00; // reserved byte (always 0)
+ data[3] = (byte) 0x03; // address type (3 - domain name)
+ data[4] = (byte) addr.length; // address length
+ System.arraycopy(addr, 0, data, 5, addr.length); // address
+ data[data.length - 2] = (byte) 0; // address port (2 bytes always 0)
+ data[data.length - 1] = (byte) 0;
+
+ return data;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java new file mode 100644 index 0000000..0d90791 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java @@ -0,0 +1,117 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.socks5;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.concurrent.TimeoutException;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.util.SyncPacketSend;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
+
+/**
+ * Implementation of a SOCKS5 client used on the initiators side. This is needed because connecting
+ * to the local SOCKS5 proxy differs form the regular way to connect to a SOCKS5 proxy. Additionally
+ * a remote SOCKS5 proxy has to be activated by the initiator before data can be transferred between
+ * the peers.
+ *
+ * @author Henning Staib
+ */
+class Socks5ClientForInitiator extends Socks5Client {
+
+ /* the XMPP connection used to communicate with the SOCKS5 proxy */
+ private Connection connection;
+
+ /* the session ID used to activate SOCKS5 stream */
+ private String sessionID;
+
+ /* the target JID used to activate SOCKS5 stream */
+ private String target;
+
+ /**
+ * Creates a new SOCKS5 client for the initiators side.
+ *
+ * @param streamHost containing network settings of the SOCKS5 proxy
+ * @param digest identifying the SOCKS5 Bytestream
+ * @param connection the XMPP connection
+ * @param sessionID the session ID of the SOCKS5 Bytestream
+ * @param target the target JID of the SOCKS5 Bytestream
+ */
+ public Socks5ClientForInitiator(StreamHost streamHost, String digest, Connection connection,
+ String sessionID, String target) {
+ super(streamHost, digest);
+ this.connection = connection;
+ this.sessionID = sessionID;
+ this.target = target;
+ }
+
+ public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException,
+ TimeoutException {
+ Socket socket = null;
+
+ // check if stream host is the local SOCKS5 proxy
+ if (this.streamHost.getJID().equals(this.connection.getUser())) {
+ Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();
+ socket = socks5Server.getSocket(this.digest);
+ if (socket == null) {
+ throw new XMPPException("target is not connected to SOCKS5 proxy");
+ }
+ }
+ else {
+ socket = super.getSocket(timeout);
+
+ try {
+ activate();
+ }
+ catch (XMPPException e) {
+ socket.close();
+ throw new XMPPException("activating SOCKS5 Bytestream failed", e);
+ }
+
+ }
+
+ return socket;
+ }
+
+ /**
+ * Activates the SOCKS5 Bytestream by sending a XMPP SOCKS5 Bytestream activation packet to the
+ * SOCKS5 proxy.
+ */
+ private void activate() throws XMPPException {
+ Bytestream activate = createStreamHostActivation();
+ // if activation fails #getReply throws an exception
+ SyncPacketSend.getReply(this.connection, activate);
+ }
+
+ /**
+ * Returns a SOCKS5 Bytestream activation packet.
+ *
+ * @return SOCKS5 Bytestream activation packet
+ */
+ private Bytestream createStreamHostActivation() {
+ Bytestream activate = new Bytestream(this.sessionID);
+ activate.setMode(null);
+ activate.setType(IQ.Type.SET);
+ activate.setTo(this.streamHost.getJID());
+
+ activate.setToActivate(this.target);
+
+ return activate;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java new file mode 100644 index 0000000..11ef7a9 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java @@ -0,0 +1,423 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.socks5;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPException;
+
+/**
+ * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by
+ * setting the <code>localSocks5ProxyEnabled</code> flag in the <code>smack-config.xml</code> or by
+ * invoking {@link SmackConfiguration#setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by
+ * default.
+ * <p>
+ * The port of the local SOCKS5 proxy can be configured by setting <code>localSocks5ProxyPort</code>
+ * in the <code>smack-config.xml</code> or by invoking
+ * {@link SmackConfiguration#setLocalSocks5ProxyPort(int)}. Default port is 7777. If you set the
+ * port to a negative value Smack tries to the absolute value and all following until it finds an
+ * open port.
+ * <p>
+ * If your application is running on a machine with multiple network interfaces or if you want to
+ * provide your public address in case you are behind a NAT router, invoke
+ * {@link #addLocalAddress(String)} or {@link #replaceLocalAddresses(List)} to modify the list of
+ * local network addresses used for outgoing SOCKS5 Bytestream requests.
+ * <p>
+ * The local SOCKS5 proxy server refuses all connections except the ones that are explicitly allowed
+ * in the process of establishing a SOCKS5 Bytestream (
+ * {@link Socks5BytestreamManager#establishSession(String)}).
+ * <p>
+ * This Implementation has the following limitations:
+ * <ul>
+ * <li>only supports the no-authentication authentication method</li>
+ * <li>only supports the <code>connect</code> command and will not answer correctly to other
+ * commands</li>
+ * <li>only supports requests with the domain address type and will not correctly answer to requests
+ * with other address types</li>
+ * </ul>
+ * (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>)
+ *
+ * @author Henning Staib
+ */
+public class Socks5Proxy {
+
+ /* SOCKS5 proxy singleton */
+ private static Socks5Proxy socks5Server;
+
+ /* reusable implementation of a SOCKS5 proxy server process */
+ private Socks5ServerProcess serverProcess;
+
+ /* thread running the SOCKS5 server process */
+ private Thread serverThread;
+
+ /* server socket to accept SOCKS5 connections */
+ private ServerSocket serverSocket;
+
+ /* assigns a connection to a digest */
+ private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>();
+
+ /* list of digests connections should be stored */
+ private final List<String> allowedConnections = Collections.synchronizedList(new LinkedList<String>());
+
+ private final Set<String> localAddresses = Collections.synchronizedSet(new LinkedHashSet<String>());
+
+ /**
+ * Private constructor.
+ */
+ private Socks5Proxy() {
+ this.serverProcess = new Socks5ServerProcess();
+
+ // add default local address
+ try {
+ this.localAddresses.add(InetAddress.getLocalHost().getHostAddress());
+ }
+ catch (UnknownHostException e) {
+ // do nothing
+ }
+
+ }
+
+ /**
+ * Returns the local SOCKS5 proxy server.
+ *
+ * @return the local SOCKS5 proxy server
+ */
+ public static synchronized Socks5Proxy getSocks5Proxy() {
+ if (socks5Server == null) {
+ socks5Server = new Socks5Proxy();
+ }
+ if (SmackConfiguration.isLocalSocks5ProxyEnabled()) {
+ socks5Server.start();
+ }
+ return socks5Server;
+ }
+
+ /**
+ * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing.
+ */
+ public synchronized void start() {
+ if (isRunning()) {
+ return;
+ }
+ try {
+ if (SmackConfiguration.getLocalSocks5ProxyPort() < 0) {
+ int port = Math.abs(SmackConfiguration.getLocalSocks5ProxyPort());
+ for (int i = 0; i < 65535 - port; i++) {
+ try {
+ this.serverSocket = new ServerSocket(port + i);
+ break;
+ }
+ catch (IOException e) {
+ // port is used, try next one
+ }
+ }
+ }
+ else {
+ this.serverSocket = new ServerSocket(SmackConfiguration.getLocalSocks5ProxyPort());
+ }
+
+ if (this.serverSocket != null) {
+ this.serverThread = new Thread(this.serverProcess);
+ this.serverThread.start();
+ }
+ }
+ catch (IOException e) {
+ // couldn't setup server
+ System.err.println("couldn't setup local SOCKS5 proxy on port "
+ + SmackConfiguration.getLocalSocks5ProxyPort() + ": " + e.getMessage());
+ }
+ }
+
+ /**
+ * Stops the local SOCKS5 proxy server. If it is not running this method does nothing.
+ */
+ public synchronized void stop() {
+ if (!isRunning()) {
+ return;
+ }
+
+ try {
+ this.serverSocket.close();
+ }
+ catch (IOException e) {
+ // do nothing
+ }
+
+ if (this.serverThread != null && this.serverThread.isAlive()) {
+ try {
+ this.serverThread.interrupt();
+ this.serverThread.join();
+ }
+ catch (InterruptedException e) {
+ // do nothing
+ }
+ }
+ this.serverThread = null;
+ this.serverSocket = null;
+
+ }
+
+ /**
+ * Adds the given address to the list of local network addresses.
+ * <p>
+ * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request.
+ * This may be necessary if your application is running on a machine with multiple network
+ * interfaces or if you want to provide your public address in case you are behind a NAT router.
+ * <p>
+ * The order of the addresses used is determined by the order you add addresses.
+ * <p>
+ * Note that the list of addresses initially contains the address returned by
+ * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of
+ * addresses by invoking {@link #replaceLocalAddresses(List)}.
+ *
+ * @param address the local network address to add
+ */
+ public void addLocalAddress(String address) {
+ if (address == null) {
+ throw new IllegalArgumentException("address may not be null");
+ }
+ this.localAddresses.add(address);
+ }
+
+ /**
+ * Removes the given address from the list of local network addresses. This address will then no
+ * longer be used of outgoing SOCKS5 Bytestream requests.
+ *
+ * @param address the local network address to remove
+ */
+ public void removeLocalAddress(String address) {
+ this.localAddresses.remove(address);
+ }
+
+ /**
+ * Returns an unmodifiable list of the local network addresses that will be used for streamhost
+ * candidates of outgoing SOCKS5 Bytestream requests.
+ *
+ * @return unmodifiable list of the local network addresses
+ */
+ public List<String> getLocalAddresses() {
+ return Collections.unmodifiableList(new ArrayList<String>(this.localAddresses));
+ }
+
+ /**
+ * Replaces the list of local network addresses.
+ * <p>
+ * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and
+ * want to define their order. This may be necessary if your application is running on a machine
+ * with multiple network interfaces or if you want to provide your public address in case you
+ * are behind a NAT router.
+ *
+ * @param addresses the new list of local network addresses
+ */
+ public void replaceLocalAddresses(List<String> addresses) {
+ if (addresses == null) {
+ throw new IllegalArgumentException("list must not be null");
+ }
+ this.localAddresses.clear();
+ this.localAddresses.addAll(addresses);
+
+ }
+
+ /**
+ * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned.
+ *
+ * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running
+ */
+ public int getPort() {
+ if (!isRunning()) {
+ return -1;
+ }
+ return this.serverSocket.getLocalPort();
+ }
+
+ /**
+ * Returns the socket for the given digest. A socket will be returned if the given digest has
+ * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer
+ * connected to the SOCKS5 proxy.
+ *
+ * @param digest identifying the connection
+ * @return socket or null if there is no socket for the given digest
+ */
+ protected Socket getSocket(String digest) {
+ return this.connectionMap.get(digest);
+ }
+
+ /**
+ * Add the given digest to the list of allowed transfers. Only connections for allowed transfers
+ * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to
+ * the local SOCKS5 proxy that don't contain an allowed digest are discarded.
+ *
+ * @param digest to be added to the list of allowed transfers
+ */
+ protected void addTransfer(String digest) {
+ this.allowedConnections.add(digest);
+ }
+
+ /**
+ * Removes the given digest from the list of allowed transfers. After invoking this method
+ * already stored connections with the given digest will be removed.
+ * <p>
+ * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error
+ * occurred while establishing the connection or if the connection is not allowed anymore.
+ *
+ * @param digest to be removed from the list of allowed transfers
+ */
+ protected void removeTransfer(String digest) {
+ this.allowedConnections.remove(digest);
+ this.connectionMap.remove(digest);
+ }
+
+ /**
+ * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise
+ * <code>false</code>.
+ *
+ * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise
+ * <code>false</code>
+ */
+ public boolean isRunning() {
+ return this.serverSocket != null;
+ }
+
+ /**
+ * Implementation of a simplified SOCKS5 proxy server.
+ */
+ private class Socks5ServerProcess implements Runnable {
+
+ public void run() {
+ while (true) {
+ Socket socket = null;
+
+ try {
+
+ if (Socks5Proxy.this.serverSocket.isClosed()
+ || Thread.currentThread().isInterrupted()) {
+ return;
+ }
+
+ // accept connection
+ socket = Socks5Proxy.this.serverSocket.accept();
+
+ // initialize connection
+ establishConnection(socket);
+
+ }
+ catch (SocketException e) {
+ /*
+ * do nothing, if caused by closing the server socket, thread will terminate in
+ * next loop
+ */
+ }
+ catch (Exception e) {
+ try {
+ if (socket != null) {
+ socket.close();
+ }
+ }
+ catch (IOException e1) {
+ /* do nothing */
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Negotiates a SOCKS5 connection and stores it on success.
+ *
+ * @param socket connection to the client
+ * @throws XMPPException if client requests a connection in an unsupported way
+ * @throws IOException if a network error occurred
+ */
+ private void establishConnection(Socket socket) throws XMPPException, IOException {
+ DataOutputStream out = new DataOutputStream(socket.getOutputStream());
+ DataInputStream in = new DataInputStream(socket.getInputStream());
+
+ // first byte is version should be 5
+ int b = in.read();
+ if (b != 5) {
+ throw new XMPPException("Only SOCKS5 supported");
+ }
+
+ // second byte number of authentication methods supported
+ b = in.read();
+
+ // read list of supported authentication methods
+ byte[] auth = new byte[b];
+ in.readFully(auth);
+
+ byte[] authMethodSelectionResponse = new byte[2];
+ authMethodSelectionResponse[0] = (byte) 0x05; // protocol version
+
+ // only authentication method 0, no authentication, supported
+ boolean noAuthMethodFound = false;
+ for (int i = 0; i < auth.length; i++) {
+ if (auth[i] == (byte) 0x00) {
+ noAuthMethodFound = true;
+ break;
+ }
+ }
+
+ if (!noAuthMethodFound) {
+ authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods
+ out.write(authMethodSelectionResponse);
+ out.flush();
+ throw new XMPPException("Authentication method not supported");
+ }
+
+ authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method
+ out.write(authMethodSelectionResponse);
+ out.flush();
+
+ // receive connection request
+ byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);
+
+ // extract digest
+ String responseDigest = new String(connectionRequest, 5, connectionRequest[4]);
+
+ // return error if digest is not allowed
+ if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
+ connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
+ out.write(connectionRequest);
+ out.flush();
+
+ throw new XMPPException("Connection is not allowed");
+ }
+
+ connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
+ out.write(connectionRequest);
+ out.flush();
+
+ // store connection
+ Socks5Proxy.this.connectionMap.put(responseDigest, socket);
+ }
+
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Utils.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Utils.java new file mode 100644 index 0000000..9c92563 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Utils.java @@ -0,0 +1,73 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.socks5;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * A collection of utility methods for SOcKS5 messages.
+ *
+ * @author Henning Staib
+ */
+class Socks5Utils {
+
+ /**
+ * Returns a SHA-1 digest of the given parameters as specified in <a
+ * href="http://xmpp.org/extensions/xep-0065.html#impl-socks5">XEP-0065</a>.
+ *
+ * @param sessionID for the SOCKS5 Bytestream
+ * @param initiatorJID JID of the initiator of a SOCKS5 Bytestream
+ * @param targetJID JID of the target of a SOCKS5 Bytestream
+ * @return SHA-1 digest of the given parameters
+ */
+ public static String createDigest(String sessionID, String initiatorJID, String targetJID) {
+ StringBuilder b = new StringBuilder();
+ b.append(sessionID).append(initiatorJID).append(targetJID);
+ return StringUtils.hash(b.toString());
+ }
+
+ /**
+ * Reads a SOCKS5 message from the given InputStream. The message can either be a SOCKS5 request
+ * message or a SOCKS5 response message.
+ * <p>
+ * (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)
+ *
+ * @param in the DataInputStream to read the message from
+ * @return the SOCKS5 message
+ * @throws IOException if a network error occurred
+ * @throws XMPPException if the SOCKS5 message contains an unsupported address type
+ */
+ public static byte[] receiveSocks5Message(DataInputStream in) throws IOException, XMPPException {
+ byte[] header = new byte[5];
+ in.readFully(header, 0, 5);
+
+ if (header[3] != (byte) 0x03) {
+ throw new XMPPException("Unsupported SOCKS5 address type");
+ }
+
+ int addressLength = header[4];
+
+ byte[] response = new byte[7 + addressLength];
+ System.arraycopy(header, 0, response, 0, header.length);
+
+ in.readFully(response, header.length, addressLength + 2);
+
+ return response;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java b/src/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java new file mode 100644 index 0000000..9e07fc3 --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java @@ -0,0 +1,474 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.socks5.packet;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * A packet representing part of a SOCKS5 Bytestream negotiation.
+ *
+ * @author Alexander Wenckus
+ */
+public class Bytestream extends IQ {
+
+ private String sessionID;
+
+ private Mode mode = Mode.tcp;
+
+ private final List<StreamHost> streamHosts = new ArrayList<StreamHost>();
+
+ private StreamHostUsed usedHost;
+
+ private Activate toActivate;
+
+ /**
+ * The default constructor
+ */
+ public Bytestream() {
+ super();
+ }
+
+ /**
+ * A constructor where the session ID can be specified.
+ *
+ * @param SID The session ID related to the negotiation.
+ * @see #setSessionID(String)
+ */
+ public Bytestream(final String SID) {
+ super();
+ setSessionID(SID);
+ }
+
+ /**
+ * Set the session ID related to the bytestream. The session ID is a unique identifier used to
+ * differentiate between stream negotiations.
+ *
+ * @param sessionID the unique session ID that identifies the transfer.
+ */
+ public void setSessionID(final String sessionID) {
+ this.sessionID = sessionID;
+ }
+
+ /**
+ * Returns the session ID related to the bytestream negotiation.
+ *
+ * @return Returns the session ID related to the bytestream negotiation.
+ * @see #setSessionID(String)
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ /**
+ * Set the transport mode. This should be put in the initiation of the interaction.
+ *
+ * @param mode the transport mode, either UDP or TCP
+ * @see Mode
+ */
+ public void setMode(final Mode mode) {
+ this.mode = mode;
+ }
+
+ /**
+ * Returns the transport mode.
+ *
+ * @return Returns the transport mode.
+ * @see #setMode(Mode)
+ */
+ public Mode getMode() {
+ return mode;
+ }
+
+ /**
+ * Adds a potential stream host that the remote user can connect to to receive the file.
+ *
+ * @param JID The JID of the stream host.
+ * @param address The internet address of the stream host.
+ * @return The added stream host.
+ */
+ public StreamHost addStreamHost(final String JID, final String address) {
+ return addStreamHost(JID, address, 0);
+ }
+
+ /**
+ * Adds a potential stream host that the remote user can connect to to receive the file.
+ *
+ * @param JID The JID of the stream host.
+ * @param address The internet address of the stream host.
+ * @param port The port on which the remote host is seeking connections.
+ * @return The added stream host.
+ */
+ public StreamHost addStreamHost(final String JID, final String address, final int port) {
+ StreamHost host = new StreamHost(JID, address);
+ host.setPort(port);
+ addStreamHost(host);
+
+ return host;
+ }
+
+ /**
+ * Adds a potential stream host that the remote user can transfer the file through.
+ *
+ * @param host The potential stream host.
+ */
+ public void addStreamHost(final StreamHost host) {
+ streamHosts.add(host);
+ }
+
+ /**
+ * Returns the list of stream hosts contained in the packet.
+ *
+ * @return Returns the list of stream hosts contained in the packet.
+ */
+ public Collection<StreamHost> getStreamHosts() {
+ return Collections.unmodifiableCollection(streamHosts);
+ }
+
+ /**
+ * Returns the stream host related to the given JID, or null if there is none.
+ *
+ * @param JID The JID of the desired stream host.
+ * @return Returns the stream host related to the given JID, or null if there is none.
+ */
+ public StreamHost getStreamHost(final String JID) {
+ if (JID == null) {
+ return null;
+ }
+ for (StreamHost host : streamHosts) {
+ if (host.getJID().equals(JID)) {
+ return host;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the count of stream hosts contained in this packet.
+ *
+ * @return Returns the count of stream hosts contained in this packet.
+ */
+ public int countStreamHosts() {
+ return streamHosts.size();
+ }
+
+ /**
+ * Upon connecting to the stream host the target of the stream replies to the initiator with the
+ * JID of the SOCKS5 host that they used.
+ *
+ * @param JID The JID of the used host.
+ */
+ public void setUsedHost(final String JID) {
+ this.usedHost = new StreamHostUsed(JID);
+ }
+
+ /**
+ * Returns the SOCKS5 host connected to by the remote user.
+ *
+ * @return Returns the SOCKS5 host connected to by the remote user.
+ */
+ public StreamHostUsed getUsedHost() {
+ return usedHost;
+ }
+
+ /**
+ * Returns the activate element of the packet sent to the proxy host to verify the identity of
+ * the initiator and match them to the appropriate stream.
+ *
+ * @return Returns the activate element of the packet sent to the proxy host to verify the
+ * identity of the initiator and match them to the appropriate stream.
+ */
+ public Activate getToActivate() {
+ return toActivate;
+ }
+
+ /**
+ * Upon the response from the target of the used host the activate packet is sent to the SOCKS5
+ * proxy. The proxy will activate the stream or return an error after verifying the identity of
+ * the initiator, using the activate packet.
+ *
+ * @param targetID The JID of the target of the file transfer.
+ */
+ public void setToActivate(final String targetID) {
+ this.toActivate = new Activate(targetID);
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<query xmlns=\"http://jabber.org/protocol/bytestreams\"");
+ if (this.getType().equals(IQ.Type.SET)) {
+ if (getSessionID() != null) {
+ buf.append(" sid=\"").append(getSessionID()).append("\"");
+ }
+ if (getMode() != null) {
+ buf.append(" mode = \"").append(getMode()).append("\"");
+ }
+ buf.append(">");
+ if (getToActivate() == null) {
+ for (StreamHost streamHost : getStreamHosts()) {
+ buf.append(streamHost.toXML());
+ }
+ }
+ else {
+ buf.append(getToActivate().toXML());
+ }
+ }
+ else if (this.getType().equals(IQ.Type.RESULT)) {
+ buf.append(">");
+ if (getUsedHost() != null) {
+ buf.append(getUsedHost().toXML());
+ }
+ // A result from the server can also contain stream hosts
+ else if (countStreamHosts() > 0) {
+ for (StreamHost host : streamHosts) {
+ buf.append(host.toXML());
+ }
+ }
+ }
+ else if (this.getType().equals(IQ.Type.GET)) {
+ return buf.append("/>").toString();
+ }
+ else {
+ return null;
+ }
+ buf.append("</query>");
+
+ return buf.toString();
+ }
+
+ /**
+ * Packet extension that represents a potential SOCKS5 proxy for the file transfer. Stream hosts
+ * are forwarded to the target of the file transfer who then chooses and connects to one.
+ *
+ * @author Alexander Wenckus
+ */
+ public static class StreamHost implements PacketExtension {
+
+ public static String NAMESPACE = "";
+
+ public static String ELEMENTNAME = "streamhost";
+
+ private final String JID;
+
+ private final String addy;
+
+ private int port = 0;
+
+ /**
+ * Default constructor.
+ *
+ * @param JID The JID of the stream host.
+ * @param address The internet address of the stream host.
+ */
+ public StreamHost(final String JID, final String address) {
+ this.JID = JID;
+ this.addy = address;
+ }
+
+ /**
+ * Returns the JID of the stream host.
+ *
+ * @return Returns the JID of the stream host.
+ */
+ public String getJID() {
+ return JID;
+ }
+
+ /**
+ * Returns the internet address of the stream host.
+ *
+ * @return Returns the internet address of the stream host.
+ */
+ public String getAddress() {
+ return addy;
+ }
+
+ /**
+ * Sets the port of the stream host.
+ *
+ * @param port The port on which the potential stream host would accept the connection.
+ */
+ public void setPort(final int port) {
+ this.port = port;
+ }
+
+ /**
+ * Returns the port on which the potential stream host would accept the connection.
+ *
+ * @return Returns the port on which the potential stream host would accept the connection.
+ */
+ public int getPort() {
+ return port;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String getElementName() {
+ return ELEMENTNAME;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(getElementName()).append(" ");
+ buf.append("jid=\"").append(getJID()).append("\" ");
+ buf.append("host=\"").append(getAddress()).append("\" ");
+ if (getPort() != 0) {
+ buf.append("port=\"").append(getPort()).append("\"");
+ }
+ else {
+ buf.append("zeroconf=\"_jabber.bytestreams\"");
+ }
+ buf.append("/>");
+
+ return buf.toString();
+ }
+ }
+
+ /**
+ * After selected a SOCKS5 stream host and successfully connecting, the target of the file
+ * transfer returns a byte stream packet with the stream host used extension.
+ *
+ * @author Alexander Wenckus
+ */
+ public static class StreamHostUsed implements PacketExtension {
+
+ public String NAMESPACE = "";
+
+ public static String ELEMENTNAME = "streamhost-used";
+
+ private final String JID;
+
+ /**
+ * Default constructor.
+ *
+ * @param JID The JID of the selected stream host.
+ */
+ public StreamHostUsed(final String JID) {
+ this.JID = JID;
+ }
+
+ /**
+ * Returns the JID of the selected stream host.
+ *
+ * @return Returns the JID of the selected stream host.
+ */
+ public String getJID() {
+ return JID;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String getElementName() {
+ return ELEMENTNAME;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<").append(getElementName()).append(" ");
+ buf.append("jid=\"").append(getJID()).append("\" ");
+ buf.append("/>");
+ return buf.toString();
+ }
+ }
+
+ /**
+ * The packet sent by the stream initiator to the stream proxy to activate the connection.
+ *
+ * @author Alexander Wenckus
+ */
+ public static class Activate implements PacketExtension {
+
+ public String NAMESPACE = "";
+
+ public static String ELEMENTNAME = "activate";
+
+ private final String target;
+
+ /**
+ * Default constructor specifying the target of the stream.
+ *
+ * @param target The target of the stream.
+ */
+ public Activate(final String target) {
+ this.target = target;
+ }
+
+ /**
+ * Returns the target of the activation.
+ *
+ * @return Returns the target of the activation.
+ */
+ public String getTarget() {
+ return target;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String getElementName() {
+ return ELEMENTNAME;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<").append(getElementName()).append(">");
+ buf.append(getTarget());
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+ }
+
+ /**
+ * The stream can be either a TCP stream or a UDP stream.
+ *
+ * @author Alexander Wenckus
+ */
+ public enum Mode {
+
+ /**
+ * A TCP based stream.
+ */
+ tcp,
+
+ /**
+ * A UDP based stream.
+ */
+ udp;
+
+ public static Mode fromName(String name) {
+ Mode mode;
+ try {
+ mode = Mode.valueOf(name);
+ }
+ catch (Exception ex) {
+ mode = tcp;
+ }
+
+ return mode;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java b/src/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java new file mode 100644 index 0000000..76f9b0c --- /dev/null +++ b/src/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java @@ -0,0 +1,82 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.bytestreams.socks5.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Parses a bytestream packet.
+ *
+ * @author Alexander Wenckus
+ */
+public class BytestreamsProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ boolean done = false;
+
+ Bytestream toReturn = new Bytestream();
+
+ String id = parser.getAttributeValue("", "sid");
+ String mode = parser.getAttributeValue("", "mode");
+
+ // streamhost
+ String JID = null;
+ String host = null;
+ String port = null;
+
+ int eventType;
+ String elementName;
+ while (!done) {
+ eventType = parser.next();
+ elementName = parser.getName();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (elementName.equals(Bytestream.StreamHost.ELEMENTNAME)) {
+ JID = parser.getAttributeValue("", "jid");
+ host = parser.getAttributeValue("", "host");
+ port = parser.getAttributeValue("", "port");
+ }
+ else if (elementName.equals(Bytestream.StreamHostUsed.ELEMENTNAME)) {
+ toReturn.setUsedHost(parser.getAttributeValue("", "jid"));
+ }
+ else if (elementName.equals(Bytestream.Activate.ELEMENTNAME)) {
+ toReturn.setToActivate(parser.getAttributeValue("", "jid"));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (elementName.equals("streamhost")) {
+ if (port == null) {
+ toReturn.addStreamHost(JID, host);
+ }
+ else {
+ toReturn.addStreamHost(JID, host, Integer.parseInt(port));
+ }
+ JID = null;
+ host = null;
+ port = null;
+ }
+ else if (elementName.equals("query")) {
+ done = true;
+ }
+ }
+ }
+
+ toReturn.setMode((Bytestream.Mode.fromName(mode)));
+ toReturn.setSessionID(id);
+ return toReturn;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/carbons/Carbon.java b/src/org/jivesoftware/smackx/carbons/Carbon.java new file mode 100644 index 0000000..588688a --- /dev/null +++ b/src/org/jivesoftware/smackx/carbons/Carbon.java @@ -0,0 +1,139 @@ +/** + * Copyright 2013 Georg Lukas + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.carbons; + +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.forward.Forwarded; +import org.jivesoftware.smackx.packet.DelayInfo; +import org.jivesoftware.smackx.provider.DelayInfoProvider; +import org.xmlpull.v1.XmlPullParser; + +/** + * Packet extension for XEP-0280: Message Carbons. This class implements + * the packet extension and a {@link PacketExtensionProvider} to parse + * message carbon copies from a packet. The extension + * <a href="http://xmpp.org/extensions/xep-0280.html">XEP-0280</a> is + * meant to synchronize a message flow to multiple presences of a user. + * + * <p>The {@link Carbon.Provider} must be registered in the + * <b>smack.properties</b> file for the elements <b>sent</b> and + * <b>received</b> with namespace <b>urn:xmpp:carbons:2</b></p> to be used. + * + * @author Georg Lukas + */ +public class Carbon implements PacketExtension { + public static final String NAMESPACE = "urn:xmpp:carbons:2"; + + private Direction dir; + private Forwarded fwd; + + public Carbon(Direction dir, Forwarded fwd) { + this.dir = dir; + this.fwd = fwd; + } + + /** + * get the direction (sent or received) of the carbon. + * + * @return the {@link Direction} of the carbon. + */ + public Direction getDirection() { + return dir; + } + + /** + * get the forwarded packet. + * + * @return the {@link Forwarded} message contained in this Carbon. + */ + public Forwarded getForwarded() { + return fwd; + } + + @Override + public String getElementName() { + return dir.toString(); + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"") + .append(getNamespace()).append("\">"); + + buf.append(fwd.toXML()); + + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + /** + * An enum to display the direction of a {@link Carbon} message. + */ + public static enum Direction { + received, + sent + } + + public static class Provider implements PacketExtensionProvider { + + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + Direction dir = Direction.valueOf(parser.getName()); + Forwarded fwd = null; + + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG && parser.getName().equals("forwarded")) { + fwd = (Forwarded)new Forwarded.Provider().parseExtension(parser); + } + else if (eventType == XmlPullParser.END_TAG && dir == Direction.valueOf(parser.getName())) + done = true; + } + if (fwd == null) + throw new Exception("sent/received must contain exactly one <forwarded> tag"); + return new Carbon(dir, fwd); + } + } + + /** + * Packet extension indicating that a message may not be carbon-copied. + */ + public static class Private implements PacketExtension { + public static final String ELEMENT = "private"; + + public String getElementName() { + return ELEMENT; + } + + public String getNamespace() { + return Carbon.NAMESPACE; + } + + public String toXML() { + return "<" + ELEMENT + " xmlns=\"" + Carbon.NAMESPACE + "\"/>"; + } + } +} diff --git a/src/org/jivesoftware/smackx/carbons/CarbonManager.java b/src/org/jivesoftware/smackx/carbons/CarbonManager.java new file mode 100644 index 0000000..f44701a --- /dev/null +++ b/src/org/jivesoftware/smackx/carbons/CarbonManager.java @@ -0,0 +1,213 @@ +/** + * Copyright 2013 Georg Lukas + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.carbons; + +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; + +/** + * Packet extension for XEP-0280: Message Carbons. This class implements + * the manager for registering {@link Carbon} support, enabling and disabling + * message carbons. + * + * You should call enableCarbons() before sending your first undirected + * presence. + * + * @author Georg Lukas + */ +public class CarbonManager { + + private static Map<Connection, CarbonManager> instances = + Collections.synchronizedMap(new WeakHashMap<Connection, CarbonManager>()); + + static { + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(Connection connection) { + new CarbonManager(connection); + } + }); + } + + private Connection connection; + private volatile boolean enabled_state = false; + + private CarbonManager(Connection connection) { + ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); + sdm.addFeature(Carbon.NAMESPACE); + this.connection = connection; + instances.put(connection, this); + } + + /** + * Obtain the CarbonManager responsible for a connection. + * + * @param connection the connection object. + * + * @return a CarbonManager instance + */ + public static CarbonManager getInstanceFor(Connection connection) { + CarbonManager carbonManager = instances.get(connection); + + if (carbonManager == null) { + carbonManager = new CarbonManager(connection); + } + + return carbonManager; + } + + private IQ carbonsEnabledIQ(final boolean new_state) { + IQ setIQ = new IQ() { + public String getChildElementXML() { + return "<" + (new_state? "enable" : "disable") + " xmlns='" + Carbon.NAMESPACE + "'/>"; + } + }; + setIQ.setType(IQ.Type.SET); + return setIQ; + } + + /** + * Returns true if XMPP Carbons are supported by the server. + * + * @return true if supported + */ + public boolean isSupportedByServer() { + try { + DiscoverInfo result = ServiceDiscoveryManager + .getInstanceFor(connection).discoverInfo(connection.getServiceName()); + return result.containsFeature(Carbon.NAMESPACE); + } + catch (XMPPException e) { + return false; + } + } + + /** + * Notify server to change the carbons state. This method returns + * immediately and changes the variable when the reply arrives. + * + * You should first check for support using isSupportedByServer(). + * + * @param new_state whether carbons should be enabled or disabled + */ + public void sendCarbonsEnabled(final boolean new_state) { + IQ setIQ = carbonsEnabledIQ(new_state); + + connection.addPacketListener(new PacketListener() { + public void processPacket(Packet packet) { + IQ result = (IQ)packet; + if (result.getType() == IQ.Type.RESULT) { + enabled_state = new_state; + } + connection.removePacketListener(this); + } + }, new PacketIDFilter(setIQ.getPacketID())); + + connection.sendPacket(setIQ); + } + + /** + * Notify server to change the carbons state. This method blocks + * some time until the server replies to the IQ and returns true on + * success. + * + * You should first check for support using isSupportedByServer(). + * + * @param new_state whether carbons should be enabled or disabled + * + * @return true if the operation was successful + */ + public boolean setCarbonsEnabled(final boolean new_state) { + if (enabled_state == new_state) + return true; + + IQ setIQ = carbonsEnabledIQ(new_state); + + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(setIQ.getPacketID())); + connection.sendPacket(setIQ); + IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + + if (result != null && result.getType() == IQ.Type.RESULT) { + enabled_state = new_state; + return true; + } + return false; + } + + /** + * Helper method to enable carbons. + * + * @return true if the operation was successful + */ + public boolean enableCarbons() { + return setCarbonsEnabled(true); + } + + /** + * Helper method to disable carbons. + * + * @return true if the operation was successful + */ + public boolean disableCarbons() { + return setCarbonsEnabled(false); + } + + /** + * Check if carbons are enabled on this connection. + */ + public boolean getCarbonsEnabled() { + return this.enabled_state; + } + + /** + * Obtain a Carbon from a message, if available. + * + * @param msg Message object to check for carbons + * + * @return a Carbon if available, null otherwise. + */ + public static Carbon getCarbon(Message msg) { + Carbon cc = (Carbon)msg.getExtension("received", Carbon.NAMESPACE); + if (cc == null) + cc = (Carbon)msg.getExtension("sent", Carbon.NAMESPACE); + return cc; + } + + /** + * Mark a message as "private", so it will not be carbon-copied. + * + * @param msg Message object to mark private + */ + public static void disableCarbons(Message msg) { + msg.addExtension(new Carbon.Private()); + } +} diff --git a/src/org/jivesoftware/smackx/commands/AdHocCommand.java b/src/org/jivesoftware/smackx/commands/AdHocCommand.java new file mode 100755 index 0000000..3077d08 --- /dev/null +++ b/src/org/jivesoftware/smackx/commands/AdHocCommand.java @@ -0,0 +1,450 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2005-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.smackx.commands;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.packet.AdHocCommandData;
+
+import java.util.List;
+
+/**
+ * An ad-hoc command is responsible for executing the provided service and
+ * storing the result of the execution. Each new request will create a new
+ * instance of the command, allowing information related to executions to be
+ * stored in it. For example suppose that a command that retrieves the list of
+ * users on a server is implemented. When the command is executed it gets that
+ * list and the result is stored as a form in the command instance, i.e. the
+ * <code>getForm</code> method retrieves a form with all the users.
+ * <p>
+ * Each command has a <tt>node</tt> that should be unique within a given JID.
+ * <p>
+ * Commands may have zero or more stages. Each stage is usually used for
+ * gathering information required for the command execution. Users are able to
+ * move forward or backward across the different stages. Commands may not be
+ * cancelled while they are being executed. However, users may request the
+ * "cancel" action when submitting a stage response indicating that the command
+ * execution should be aborted. Thus, releasing any collected information.
+ * Commands that require user interaction (i.e. have more than one stage) will
+ * have to provide the data forms the user must complete in each stage and the
+ * allowed actions the user might perform during each stage (e.g. go to the
+ * previous stage or go to the next stage).
+ * <p>
+ * All the actions may throw an XMPPException if there is a problem executing
+ * them. The <code>XMPPError</code> of that exception may have some specific
+ * information about the problem. The possible extensions are:
+ *
+ * <li><i>malformed-action</i>. Extension of a <i>bad-request</i> error.</li>
+ * <li><i>bad-action</i>. Extension of a <i>bad-request</i> error.</li>
+ * <li><i>bad-locale</i>. Extension of a <i>bad-request</i> error.</li>
+ * <li><i>bad-payload</i>. Extension of a <i>bad-request</i> error.</li>
+ * <li><i>bad-sessionid</i>. Extension of a <i>bad-request</i> error.</li>
+ * <li><i>session-expired</i>. Extension of a <i>not-allowed</i> error.</li>
+ * <p>
+ * See the <code>SpecificErrorCondition</code> class for detailed description
+ * of each one.
+ * <p>
+ * Use the <code>getSpecificErrorConditionFrom</code> to obtain the specific
+ * information from an <code>XMPPError</code>.
+ *
+ * @author Gabriel Guardincerri
+ *
+ */
+public abstract class AdHocCommand {
+ // TODO: Analyze the redesign of command by having an ExecutionResponse as a
+ // TODO: result to the execution of every action. That result should have all the
+ // TODO: information related to the execution, e.g. the form to fill. Maybe this
+ // TODO: design is more intuitive and simpler than the current one that has all in
+ // TODO: one class.
+
+ private AdHocCommandData data;
+
+ public AdHocCommand() {
+ super();
+ data = new AdHocCommandData();
+ }
+
+ /**
+ * Returns the specific condition of the <code>error</code> or <tt>null</tt> if the
+ * error doesn't have any.
+ *
+ * @param error the error the get the specific condition from.
+ * @return the specific condition of this error, or null if it doesn't have
+ * any.
+ */
+ public static SpecificErrorCondition getSpecificErrorCondition(XMPPError error) {
+ // This method is implemented to provide an easy way of getting a packet
+ // extension of the XMPPError.
+ for (SpecificErrorCondition condition : SpecificErrorCondition.values()) {
+ if (error.getExtension(condition.toString(),
+ AdHocCommandData.SpecificError.namespace) != null) {
+ return condition;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Set the the human readable name of the command, usually used for
+ * displaying in a UI.
+ *
+ * @param name the name.
+ */
+ public void setName(String name) {
+ data.setName(name);
+ }
+
+ /**
+ * Returns the human readable name of the command.
+ *
+ * @return the human readable name of the command
+ */
+ public String getName() {
+ return data.getName();
+ }
+
+ /**
+ * Sets the unique identifier of the command. This value must be unique for
+ * the <code>OwnerJID</code>.
+ *
+ * @param node the unique identifier of the command.
+ */
+ public void setNode(String node) {
+ data.setNode(node);
+ }
+
+ /**
+ * Returns the unique identifier of the command. It is unique for the
+ * <code>OwnerJID</code>.
+ *
+ * @return the unique identifier of the command.
+ */
+ public String getNode() {
+ return data.getNode();
+ }
+
+ /**
+ * Returns the full JID of the owner of this command. This JID is the "to" of a
+ * execution request.
+ *
+ * @return the owner JID.
+ */
+ public abstract String getOwnerJID();
+
+ /**
+ * Returns the notes that the command has at the current stage.
+ *
+ * @return a list of notes.
+ */
+ public List<AdHocCommandNote> getNotes() {
+ return data.getNotes();
+ }
+
+ /**
+ * Adds a note to the current stage. This should be used when setting a
+ * response to the execution of an action. All the notes added here are
+ * returned by the {@link #getNotes} method during the current stage.
+ * Once the stage changes all the notes are discarded.
+ *
+ * @param note the note.
+ */
+ protected void addNote(AdHocCommandNote note) {
+ data.addNote(note);
+ }
+
+ public String getRaw() {
+ return data.getChildElementXML();
+ }
+
+ /**
+ * Returns the form of the current stage. Usually it is the form that must
+ * be answered to execute the next action. If that is the case it should be
+ * used by the requester to fill all the information that the executor needs
+ * to continue to the next stage. It can also be the result of the
+ * execution.
+ *
+ * @return the form of the current stage to fill out or the result of the
+ * execution.
+ */
+ public Form getForm() {
+ if (data.getForm() == null) {
+ return null;
+ }
+ else {
+ return new Form(data.getForm());
+ }
+ }
+
+ /**
+ * Sets the form of the current stage. This should be used when setting a
+ * response. It could be a form to fill out the information needed to go to
+ * the next stage or the result of an execution.
+ *
+ * @param form the form of the current stage to fill out or the result of the
+ * execution.
+ */
+ protected void setForm(Form form) {
+ data.setForm(form.getDataFormToSend());
+ }
+
+ /**
+ * Executes the command. This is invoked only on the first stage of the
+ * command. It is invoked on every command. If there is a problem executing
+ * the command it throws an XMPPException.
+ *
+ * @throws XMPPException if there is an error executing the command.
+ */
+ public abstract void execute() throws XMPPException;
+
+ /**
+ * Executes the next action of the command with the information provided in
+ * the <code>response</code>. This form must be the answer form of the
+ * previous stage. This method will be only invoked for commands that have one
+ * or more stages. If there is a problem executing the command it throws an
+ * XMPPException.
+ *
+ * @param response the form answer of the previous stage.
+ * @throws XMPPException if there is a problem executing the command.
+ */
+ public abstract void next(Form response) throws XMPPException;
+
+ /**
+ * Completes the command execution with the information provided in the
+ * <code>response</code>. This form must be the answer form of the
+ * previous stage. This method will be only invoked for commands that have one
+ * or more stages. If there is a problem executing the command it throws an
+ * XMPPException.
+ *
+ * @param response the form answer of the previous stage.
+ * @throws XMPPException if there is a problem executing the command.
+ */
+ public abstract void complete(Form response) throws XMPPException;
+
+ /**
+ * Goes to the previous stage. The requester is asking to re-send the
+ * information of the previous stage. The command must change it state to
+ * the previous one. If there is a problem executing the command it throws
+ * an XMPPException.
+ *
+ * @throws XMPPException if there is a problem executing the command.
+ */
+ public abstract void prev() throws XMPPException;
+
+ /**
+ * Cancels the execution of the command. This can be invoked on any stage of
+ * the execution. If there is a problem executing the command it throws an
+ * XMPPException.
+ *
+ * @throws XMPPException if there is a problem executing the command.
+ */
+ public abstract void cancel() throws XMPPException;
+
+ /**
+ * Returns a collection with the allowed actions based on the current stage.
+ * Possible actions are: {@link Action#prev prev}, {@link Action#next next} and
+ * {@link Action#complete complete}. This method will be only invoked for commands that
+ * have one or more stages.
+ *
+ * @return a collection with the allowed actions based on the current stage
+ * as defined in the SessionData.
+ */
+ protected List<Action> getActions() {
+ return data.getActions();
+ }
+
+ /**
+ * Add an action to the current stage available actions. This should be used
+ * when creating a response.
+ *
+ * @param action the action.
+ */
+ protected void addActionAvailable(Action action) {
+ data.addAction(action);
+ }
+
+ /**
+ * Returns the action available for the current stage which is
+ * considered the equivalent to "execute". When the requester sends his
+ * reply, if no action was defined in the command then the action will be
+ * assumed "execute" thus assuming the action returned by this method. This
+ * method will never be invoked for commands that have no stages.
+ *
+ * @return the action available for the current stage which is considered
+ * the equivalent to "execute".
+ */
+ protected Action getExecuteAction() {
+ return data.getExecuteAction();
+ }
+
+ /**
+ * Sets which of the actions available for the current stage is
+ * considered the equivalent to "execute". This should be used when setting
+ * a response. When the requester sends his reply, if no action was defined
+ * in the command then the action will be assumed "execute" thus assuming
+ * the action returned by this method.
+ *
+ * @param action the action.
+ */
+ protected void setExecuteAction(Action action) {
+ data.setExecuteAction(action);
+ }
+
+ /**
+ * Returns the status of the current stage.
+ *
+ * @return the current status.
+ */
+ public Status getStatus() {
+ return data.getStatus();
+ }
+
+ /**
+ * Sets the data of the current stage. This should not used.
+ *
+ * @param data the data.
+ */
+ void setData(AdHocCommandData data) {
+ this.data = data;
+ }
+
+ /**
+ * Gets the data of the current stage. This should not used.
+ *
+ * @return the data.
+ */
+ AdHocCommandData getData() {
+ return data;
+ }
+
+ /**
+ * Returns true if the <code>action</code> is available in the current stage.
+ * The {@link Action#cancel cancel} action is always allowed. To define the
+ * available actions use the <code>addActionAvailable</code> method.
+ *
+ * @param action
+ * The action to check if it is available.
+ * @return True if the action is available for the current stage.
+ */
+ protected boolean isValidAction(Action action) {
+ return getActions().contains(action) || Action.cancel.equals(action);
+ }
+
+ /**
+ * The status of the stage in the adhoc command.
+ */
+ public enum Status {
+
+ /**
+ * The command is being executed.
+ */
+ executing,
+
+ /**
+ * The command has completed. The command session has ended.
+ */
+ completed,
+
+ /**
+ * The command has been canceled. The command session has ended.
+ */
+ canceled
+ }
+
+ public enum Action {
+
+ /**
+ * The command should be executed or continue to be executed. This is
+ * the default value.
+ */
+ execute,
+
+ /**
+ * The command should be canceled.
+ */
+ cancel,
+
+ /**
+ * The command should be digress to the previous stage of execution.
+ */
+ prev,
+
+ /**
+ * The command should progress to the next stage of execution.
+ */
+ next,
+
+ /**
+ * The command should be completed (if possible).
+ */
+ complete,
+
+ /**
+ * The action is unknow. This is used when a recieved message has an
+ * unknown action. It must not be used to send an execution request.
+ */
+ unknown
+ }
+
+ public enum SpecificErrorCondition {
+
+ /**
+ * The responding JID cannot accept the specified action.
+ */
+ badAction("bad-action"),
+
+ /**
+ * The responding JID does not understand the specified action.
+ */
+ malformedAction("malformed-action"),
+
+ /**
+ * The responding JID cannot accept the specified language/locale.
+ */
+ badLocale("bad-locale"),
+
+ /**
+ * The responding JID cannot accept the specified payload (e.g. the data
+ * form did not provide one or more required fields).
+ */
+ badPayload("bad-payload"),
+
+ /**
+ * The responding JID cannot accept the specified sessionid.
+ */
+ badSessionid("bad-sessionid"),
+
+ /**
+ * The requesting JID specified a sessionid that is no longer active
+ * (either because it was completed, canceled, or timed out).
+ */
+ sessionExpired("session-expired");
+
+ private String value;
+
+ SpecificErrorCondition(String value) {
+ this.value = value;
+ }
+
+ public String toString() {
+ return value;
+ }
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/commands/AdHocCommandManager.java b/src/org/jivesoftware/smackx/commands/AdHocCommandManager.java new file mode 100755 index 0000000..8dac999 --- /dev/null +++ b/src/org/jivesoftware/smackx/commands/AdHocCommandManager.java @@ -0,0 +1,753 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2005-2008 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jivesoftware.smackx.commands;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.NodeInformationProvider;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.commands.AdHocCommand.Action;
+import org.jivesoftware.smackx.commands.AdHocCommand.Status;
+import org.jivesoftware.smackx.packet.AdHocCommandData;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * An AdHocCommandManager is responsible for keeping the list of available
+ * commands offered by a service and for processing commands requests.
+ *
+ * Pass in a Connection instance to
+ * {@link #getAddHocCommandsManager(org.jivesoftware.smack.Connection)} in order to
+ * get an instance of this class.
+ *
+ * @author Gabriel Guardincerri
+ */
+public class AdHocCommandManager {
+
+ private static final String DISCO_NAMESPACE = "http://jabber.org/protocol/commands";
+
+ private static final String discoNode = DISCO_NAMESPACE;
+
+ /**
+ * The session time out in seconds.
+ */
+ private static final int SESSION_TIMEOUT = 2 * 60;
+
+ /**
+ * Map a Connection with it AdHocCommandManager. This map have a key-value
+ * pair for every active connection.
+ */
+ private static Map<Connection, AdHocCommandManager> instances =
+ new ConcurrentHashMap<Connection, AdHocCommandManager>();
+
+ /**
+ * Register the listener for all the connection creations. When a new
+ * connection is created a new AdHocCommandManager is also created and
+ * related to that connection.
+ */
+ static {
+ Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+ public void connectionCreated(Connection connection) {
+ new AdHocCommandManager(connection);
+ }
+ });
+ }
+
+ /**
+ * Returns the <code>AdHocCommandManager</code> related to the
+ * <code>connection</code>.
+ *
+ * @param connection the XMPP connection.
+ * @return the AdHocCommandManager associated with the connection.
+ */
+ public static AdHocCommandManager getAddHocCommandsManager(Connection connection) {
+ return instances.get(connection);
+ }
+
+ /**
+ * Thread that reaps stale sessions.
+ */
+ private Thread sessionsSweeper;
+
+ /**
+ * The Connection that this instances of AdHocCommandManager manages
+ */
+ private Connection connection;
+
+ /**
+ * Map a command node with its AdHocCommandInfo. Note: Key=command node,
+ * Value=command. Command node matches the node attribute sent by command
+ * requesters.
+ */
+ private Map<String, AdHocCommandInfo> commands = Collections
+ .synchronizedMap(new WeakHashMap<String, AdHocCommandInfo>());
+
+ /**
+ * Map a command session ID with the instance LocalCommand. The LocalCommand
+ * is the an objects that has all the information of the current state of
+ * the command execution. Note: Key=session ID, Value=LocalCommand. Session
+ * ID matches the sessionid attribute sent by command responders.
+ */
+ private Map<String, LocalCommand> executingCommands = new ConcurrentHashMap<String, LocalCommand>();
+
+ private AdHocCommandManager(Connection connection) {
+ super();
+ this.connection = connection;
+ init();
+ }
+
+ /**
+ * Registers a new command with this command manager, which is related to a
+ * connection. The <tt>node</tt> is an unique identifier of that command for
+ * the connection related to this command manager. The <tt>name</tt> is the
+ * human readable name of the command. The <tt>class</tt> is the class of
+ * the command, which must extend {@link LocalCommand} and have a default
+ * constructor.
+ *
+ * @param node the unique identifier of the command.
+ * @param name the human readable name of the command.
+ * @param clazz the class of the command, which must extend {@link LocalCommand}.
+ */
+ public void registerCommand(String node, String name, final Class<? extends LocalCommand> clazz) {
+ registerCommand(node, name, new LocalCommandFactory() {
+ public LocalCommand getInstance() throws InstantiationException, IllegalAccessException {
+ return clazz.newInstance();
+ }
+ });
+ }
+
+ /**
+ * Registers a new command with this command manager, which is related to a
+ * connection. The <tt>node</tt> is an unique identifier of that
+ * command for the connection related to this command manager. The <tt>name</tt>
+ * is the human readeale name of the command. The <tt>factory</tt> generates
+ * new instances of the command.
+ *
+ * @param node the unique identifier of the command.
+ * @param name the human readable name of the command.
+ * @param factory a factory to create new instances of the command.
+ */
+ public void registerCommand(String node, final String name, LocalCommandFactory factory) {
+ AdHocCommandInfo commandInfo = new AdHocCommandInfo(node, name, connection.getUser(), factory);
+
+ commands.put(node, commandInfo);
+ // Set the NodeInformationProvider that will provide information about
+ // the added command
+ ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(node,
+ new NodeInformationProvider() {
+ public List<DiscoverItems.Item> getNodeItems() {
+ return null;
+ }
+
+ public List<String> getNodeFeatures() {
+ List<String> answer = new ArrayList<String>();
+ answer.add(DISCO_NAMESPACE);
+ // TODO: check if this service is provided by the
+ // TODO: current connection.
+ answer.add("jabber:x:data");
+ return answer;
+ }
+
+ public List<DiscoverInfo.Identity> getNodeIdentities() {
+ List<DiscoverInfo.Identity> answer = new ArrayList<DiscoverInfo.Identity>();
+ DiscoverInfo.Identity identity = new DiscoverInfo.Identity(
+ "automation", name, "command-node");
+ answer.add(identity);
+ return answer;
+ }
+
+ @Override
+ public List<PacketExtension> getNodePacketExtensions() {
+ return null;
+ }
+
+ });
+ }
+
+ /**
+ * Discover the commands of an specific JID. The <code>jid</code> is a
+ * full JID.
+ *
+ * @param jid the full JID to retrieve the commands for.
+ * @return the discovered items.
+ * @throws XMPPException if the operation failed for some reason.
+ */
+ public DiscoverItems discoverCommands(String jid) throws XMPPException {
+ ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager
+ .getInstanceFor(connection);
+ return serviceDiscoveryManager.discoverItems(jid, discoNode);
+ }
+
+ /**
+ * Publish the commands to an specific JID.
+ *
+ * @param jid the full JID to publish the commands to.
+ * @throws XMPPException if the operation failed for some reason.
+ */
+ public void publishCommands(String jid) throws XMPPException {
+ ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager
+ .getInstanceFor(connection);
+
+ // Collects the commands to publish as items
+ DiscoverItems discoverItems = new DiscoverItems();
+ Collection<AdHocCommandInfo> xCommandsList = getRegisteredCommands();
+
+ for (AdHocCommandInfo info : xCommandsList) {
+ DiscoverItems.Item item = new DiscoverItems.Item(info.getOwnerJID());
+ item.setName(info.getName());
+ item.setNode(info.getNode());
+ discoverItems.addItem(item);
+ }
+
+ serviceDiscoveryManager.publishItems(jid, discoNode, discoverItems);
+ }
+
+ /**
+ * Returns a command that represents an instance of a command in a remote
+ * host. It is used to execute remote commands. The concept is similar to
+ * RMI. Every invocation on this command is equivalent to an invocation in
+ * the remote command.
+ *
+ * @param jid the full JID of the host of the remote command
+ * @param node the identifier of the command
+ * @return a local instance equivalent to the remote command.
+ */
+ public RemoteCommand getRemoteCommand(String jid, String node) {
+ return new RemoteCommand(connection, node, jid);
+ }
+
+ /**
+ * <ul>
+ * <li>Adds listeners to the connection</li>
+ * <li>Registers the ad-hoc command feature to the ServiceDiscoveryManager</li>
+ * <li>Registers the items of the feature</li>
+ * <li>Adds packet listeners to handle execution requests</li>
+ * <li>Creates and start the session sweeper</li>
+ * </ul>
+ */
+ private void init() {
+ // Register the new instance and associate it with the connection
+ instances.put(connection, this);
+
+ // Add a listener to the connection that removes the registered instance
+ // when the connection is closed
+ connection.addConnectionListener(new ConnectionListener() {
+ public void connectionClosed() {
+ // Unregister this instance since the connection has been closed
+ instances.remove(connection);
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ // Unregister this instance since the connection has been closed
+ instances.remove(connection);
+ }
+
+ public void reconnectionSuccessful() {
+ // Register this instance since the connection has been
+ // reestablished
+ instances.put(connection, AdHocCommandManager.this);
+ }
+
+ public void reconnectingIn(int seconds) {
+ // Nothing to do
+ }
+
+ public void reconnectionFailed(Exception e) {
+ // Nothing to do
+ }
+ });
+
+ // Add the feature to the service discovery manage to show that this
+ // connection supports the AdHoc-Commands protocol.
+ // This information will be used when another client tries to
+ // discover whether this client supports AdHoc-Commands or not.
+ ServiceDiscoveryManager.getInstanceFor(connection).addFeature(
+ DISCO_NAMESPACE);
+
+ // Set the NodeInformationProvider that will provide information about
+ // which AdHoc-Commands are registered, whenever a disco request is
+ // received
+ ServiceDiscoveryManager.getInstanceFor(connection)
+ .setNodeInformationProvider(discoNode,
+ new NodeInformationProvider() {
+ public List<DiscoverItems.Item> getNodeItems() {
+
+ List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>();
+ Collection<AdHocCommandInfo> commandsList = getRegisteredCommands();
+
+ for (AdHocCommandInfo info : commandsList) {
+ DiscoverItems.Item item = new DiscoverItems.Item(
+ info.getOwnerJID());
+ item.setName(info.getName());
+ item.setNode(info.getNode());
+ answer.add(item);
+ }
+
+ return answer;
+ }
+
+ public List<String> getNodeFeatures() {
+ return null;
+ }
+
+ public List<Identity> getNodeIdentities() {
+ return null;
+ }
+
+ @Override
+ public List<PacketExtension> getNodePacketExtensions() {
+ return null;
+ }
+ });
+
+ // The packet listener and the filter for processing some AdHoc Commands
+ // Packets
+ PacketListener listener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ AdHocCommandData requestData = (AdHocCommandData) packet;
+ processAdHocCommand(requestData);
+ }
+ };
+
+ PacketFilter filter = new PacketTypeFilter(AdHocCommandData.class);
+ connection.addPacketListener(listener, filter);
+
+ sessionsSweeper = null;
+ }
+
+ /**
+ * Process the AdHoc-Command packet that request the execution of some
+ * action of a command. If this is the first request, this method checks,
+ * before executing the command, if:
+ * <ul>
+ * <li>The requested command exists</li>
+ * <li>The requester has permissions to execute it</li>
+ * <li>The command has more than one stage, if so, it saves the command and
+ * session ID for further use</li>
+ * </ul>
+ *
+ * <br>
+ * <br>
+ * If this is not the first request, this method checks, before executing
+ * the command, if:
+ * <ul>
+ * <li>The session ID of the request was stored</li>
+ * <li>The session life do not exceed the time out</li>
+ * <li>The action to execute is one of the available actions</li>
+ * </ul>
+ *
+ * @param requestData
+ * the packet to process.
+ */
+ private void processAdHocCommand(AdHocCommandData requestData) {
+ // Only process requests of type SET
+ if (requestData.getType() != IQ.Type.SET) {
+ return;
+ }
+
+ // Creates the response with the corresponding data
+ AdHocCommandData response = new AdHocCommandData();
+ response.setTo(requestData.getFrom());
+ response.setPacketID(requestData.getPacketID());
+ response.setNode(requestData.getNode());
+ response.setId(requestData.getTo());
+
+ String sessionId = requestData.getSessionID();
+ String commandNode = requestData.getNode();
+
+ if (sessionId == null) {
+ // A new execution request has been received. Check that the
+ // command exists
+ if (!commands.containsKey(commandNode)) {
+ // Requested command does not exist so return
+ // item_not_found error.
+ respondError(response, XMPPError.Condition.item_not_found);
+ return;
+ }
+
+ // Create new session ID
+ sessionId = StringUtils.randomString(15);
+
+ try {
+ // Create a new instance of the command with the
+ // corresponding sessioid
+ LocalCommand command = newInstanceOfCmd(commandNode, sessionId);
+
+ response.setType(IQ.Type.RESULT);
+ command.setData(response);
+
+ // Check that the requester has enough permission.
+ // Answer forbidden error if requester permissions are not
+ // enough to execute the requested command
+ if (!command.hasPermission(requestData.getFrom())) {
+ respondError(response, XMPPError.Condition.forbidden);
+ return;
+ }
+
+ Action action = requestData.getAction();
+
+ // If the action is unknown then respond an error.
+ if (action != null && action.equals(Action.unknown)) {
+ respondError(response, XMPPError.Condition.bad_request,
+ AdHocCommand.SpecificErrorCondition.malformedAction);
+ return;
+ }
+
+ // If the action is not execute, then it is an invalid action.
+ if (action != null && !action.equals(Action.execute)) {
+ respondError(response, XMPPError.Condition.bad_request,
+ AdHocCommand.SpecificErrorCondition.badAction);
+ return;
+ }
+
+ // Increase the state number, so the command knows in witch
+ // stage it is
+ command.incrementStage();
+ // Executes the command
+ command.execute();
+
+ if (command.isLastStage()) {
+ // If there is only one stage then the command is completed
+ response.setStatus(Status.completed);
+ }
+ else {
+ // Else it is still executing, and is registered to be
+ // available for the next call
+ response.setStatus(Status.executing);
+ executingCommands.put(sessionId, command);
+ // See if the session reaping thread is started. If not, start it.
+ if (sessionsSweeper == null) {
+ sessionsSweeper = new Thread(new Runnable() {
+ public void run() {
+ while (true) {
+ for (String sessionId : executingCommands.keySet()) {
+ LocalCommand command = executingCommands.get(sessionId);
+ // Since the command could be removed in the meanwhile
+ // of getting the key and getting the value - by a
+ // processed packet. We must check if it still in the
+ // map.
+ if (command != null) {
+ long creationStamp = command.getCreationDate();
+ // Check if the Session data has expired (default is
+ // 10 minutes)
+ // To remove it from the session list it waits for
+ // the double of the of time out time. This is to
+ // let
+ // the requester know why his execution request is
+ // not accepted. If the session is removed just
+ // after the time out, then whe the user request to
+ // continue the execution he will recieved an
+ // invalid session error and not a time out error.
+ if (System.currentTimeMillis() - creationStamp > SESSION_TIMEOUT * 1000 * 2) {
+ // Remove the expired session
+ executingCommands.remove(sessionId);
+ }
+ }
+ }
+ try {
+ Thread.sleep(1000);
+ }
+ catch (InterruptedException ie) {
+ // Ignore.
+ }
+ }
+ }
+
+ });
+ sessionsSweeper.setDaemon(true);
+ sessionsSweeper.start();
+ }
+ }
+
+ // Sends the response packet
+ connection.sendPacket(response);
+
+ }
+ catch (XMPPException e) {
+ // If there is an exception caused by the next, complete,
+ // prev or cancel method, then that error is returned to the
+ // requester.
+ XMPPError error = e.getXMPPError();
+
+ // If the error type is cancel, then the execution is
+ // canceled therefore the status must show that, and the
+ // command be removed from the executing list.
+ if (XMPPError.Type.CANCEL.equals(error.getType())) {
+ response.setStatus(Status.canceled);
+ executingCommands.remove(sessionId);
+ }
+ respondError(response, error);
+ e.printStackTrace();
+ }
+ }
+ else {
+ LocalCommand command = executingCommands.get(sessionId);
+
+ // Check that a command exists for the specified sessionID
+ // This also handles if the command was removed in the meanwhile
+ // of getting the key and the value of the map.
+ if (command == null) {
+ respondError(response, XMPPError.Condition.bad_request,
+ AdHocCommand.SpecificErrorCondition.badSessionid);
+ return;
+ }
+
+ // Check if the Session data has expired (default is 10 minutes)
+ long creationStamp = command.getCreationDate();
+ if (System.currentTimeMillis() - creationStamp > SESSION_TIMEOUT * 1000) {
+ // Remove the expired session
+ executingCommands.remove(sessionId);
+
+ // Answer a not_allowed error (session-expired)
+ respondError(response, XMPPError.Condition.not_allowed,
+ AdHocCommand.SpecificErrorCondition.sessionExpired);
+ return;
+ }
+
+ /*
+ * Since the requester could send two requests for the same
+ * executing command i.e. the same session id, all the execution of
+ * the action must be synchronized to avoid inconsistencies.
+ */
+ synchronized (command) {
+ Action action = requestData.getAction();
+
+ // If the action is unknown the respond an error
+ if (action != null && action.equals(Action.unknown)) {
+ respondError(response, XMPPError.Condition.bad_request,
+ AdHocCommand.SpecificErrorCondition.malformedAction);
+ return;
+ }
+
+ // If the user didn't specify an action or specify the execute
+ // action then follow the actual default execute action
+ if (action == null || Action.execute.equals(action)) {
+ action = command.getExecuteAction();
+ }
+
+ // Check that the specified action was previously
+ // offered
+ if (!command.isValidAction(action)) {
+ respondError(response, XMPPError.Condition.bad_request,
+ AdHocCommand.SpecificErrorCondition.badAction);
+ return;
+ }
+
+ try {
+ // TODO: Check that all the requierd fields of the form are
+ // TODO: filled, if not throw an exception. This will simplify the
+ // TODO: construction of new commands
+
+ // Since all errors were passed, the response is now a
+ // result
+ response.setType(IQ.Type.RESULT);
+
+ // Set the new data to the command.
+ command.setData(response);
+
+ if (Action.next.equals(action)) {
+ command.incrementStage();
+ command.next(new Form(requestData.getForm()));
+ if (command.isLastStage()) {
+ // If it is the last stage then the command is
+ // completed
+ response.setStatus(Status.completed);
+ }
+ else {
+ // Otherwise it is still executing
+ response.setStatus(Status.executing);
+ }
+ }
+ else if (Action.complete.equals(action)) {
+ command.incrementStage();
+ command.complete(new Form(requestData.getForm()));
+ response.setStatus(Status.completed);
+ // Remove the completed session
+ executingCommands.remove(sessionId);
+ }
+ else if (Action.prev.equals(action)) {
+ command.decrementStage();
+ command.prev();
+ }
+ else if (Action.cancel.equals(action)) {
+ command.cancel();
+ response.setStatus(Status.canceled);
+ // Remove the canceled session
+ executingCommands.remove(sessionId);
+ }
+
+ connection.sendPacket(response);
+ }
+ catch (XMPPException e) {
+ // If there is an exception caused by the next, complete,
+ // prev or cancel method, then that error is returned to the
+ // requester.
+ XMPPError error = e.getXMPPError();
+
+ // If the error type is cancel, then the execution is
+ // canceled therefore the status must show that, and the
+ // command be removed from the executing list.
+ if (XMPPError.Type.CANCEL.equals(error.getType())) {
+ response.setStatus(Status.canceled);
+ executingCommands.remove(sessionId);
+ }
+ respondError(response, error);
+
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * Responds an error with an specific condition.
+ *
+ * @param response the response to send.
+ * @param condition the condition of the error.
+ */
+ private void respondError(AdHocCommandData response,
+ XMPPError.Condition condition) {
+ respondError(response, new XMPPError(condition));
+ }
+
+ /**
+ * Responds an error with an specific condition.
+ *
+ * @param response the response to send.
+ * @param condition the condition of the error.
+ * @param specificCondition the adhoc command error condition.
+ */
+ private void respondError(AdHocCommandData response, XMPPError.Condition condition,
+ AdHocCommand.SpecificErrorCondition specificCondition)
+ {
+ XMPPError error = new XMPPError(condition);
+ error.addExtension(new AdHocCommandData.SpecificError(specificCondition));
+ respondError(response, error);
+ }
+
+ /**
+ * Responds an error with an specific error.
+ *
+ * @param response the response to send.
+ * @param error the error to send.
+ */
+ private void respondError(AdHocCommandData response, XMPPError error) {
+ response.setType(IQ.Type.ERROR);
+ response.setError(error);
+ connection.sendPacket(response);
+ }
+
+ /**
+ * Creates a new instance of a command to be used by a new execution request
+ *
+ * @param commandNode the command node that identifies it.
+ * @param sessionID the session id of this execution.
+ * @return the command instance to execute.
+ * @throws XMPPException if there is problem creating the new instance.
+ */
+ private LocalCommand newInstanceOfCmd(String commandNode, String sessionID)
+ throws XMPPException
+ {
+ AdHocCommandInfo commandInfo = commands.get(commandNode);
+ LocalCommand command;
+ try {
+ command = (LocalCommand) commandInfo.getCommandInstance();
+ command.setSessionID(sessionID);
+ command.setName(commandInfo.getName());
+ command.setNode(commandInfo.getNode());
+ }
+ catch (InstantiationException e) {
+ e.printStackTrace();
+ throw new XMPPException(new XMPPError(
+ XMPPError.Condition.interna_server_error));
+ }
+ catch (IllegalAccessException e) {
+ e.printStackTrace();
+ throw new XMPPException(new XMPPError(
+ XMPPError.Condition.interna_server_error));
+ }
+ return command;
+ }
+
+ /**
+ * Returns the registered commands of this command manager, which is related
+ * to a connection.
+ *
+ * @return the registered commands.
+ */
+ private Collection<AdHocCommandInfo> getRegisteredCommands() {
+ return commands.values();
+ }
+
+ /**
+ * Stores ad-hoc command information.
+ */
+ private static class AdHocCommandInfo {
+
+ private String node;
+ private String name;
+ private String ownerJID;
+ private LocalCommandFactory factory;
+
+ public AdHocCommandInfo(String node, String name, String ownerJID,
+ LocalCommandFactory factory)
+ {
+ this.node = node;
+ this.name = name;
+ this.ownerJID = ownerJID;
+ this.factory = factory;
+ }
+
+ public LocalCommand getCommandInstance() throws InstantiationException,
+ IllegalAccessException
+ {
+ return factory.getInstance();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getNode() {
+ return node;
+ }
+
+ public String getOwnerJID() {
+ return ownerJID;
+ }
+ }
+} diff --git a/src/org/jivesoftware/smackx/commands/AdHocCommandNote.java b/src/org/jivesoftware/smackx/commands/AdHocCommandNote.java new file mode 100755 index 0000000..10dedbe --- /dev/null +++ b/src/org/jivesoftware/smackx/commands/AdHocCommandNote.java @@ -0,0 +1,86 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2005-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.smackx.commands;
+
+/**
+ * Notes can be added to a command execution response. A note has a type and value.
+ *
+ * @author Gabriel Guardincerri
+ */
+public class AdHocCommandNote {
+
+ private Type type;
+ private String value;
+
+ /**
+ * Creates a new adhoc command note with the specified type and value.
+ *
+ * @param type the type of the note.
+ * @param value the value of the note.
+ */
+ public AdHocCommandNote(Type type, String value) {
+ this.type = type;
+ this.value = value;
+ }
+
+ /**
+ * Returns the value or message of the note.
+ *
+ * @return the value or message of the note.
+ */
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Return the type of the note.
+ *
+ * @return the type of the note.
+ */
+ public Type getType() {
+ return type;
+ }
+
+ /**
+ * Represents a note type.
+ */
+ public enum Type {
+
+ /**
+ * The note is informational only. This is not really an exceptional
+ * condition.
+ */
+ info,
+
+ /**
+ * The note indicates a warning. Possibly due to illogical (yet valid)
+ * data.
+ */
+ warn,
+
+ /**
+ * The note indicates an error. The text should indicate the reason for
+ * the error.
+ */
+ error
+ }
+
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/commands/LocalCommand.java b/src/org/jivesoftware/smackx/commands/LocalCommand.java new file mode 100755 index 0000000..627d30e --- /dev/null +++ b/src/org/jivesoftware/smackx/commands/LocalCommand.java @@ -0,0 +1,169 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2005-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.smackx.commands;
+
+import org.jivesoftware.smackx.packet.AdHocCommandData;
+
+/**
+ * Represents a command that can be executed locally from a remote location. This
+ * class must be extended to implement an specific ad-hoc command. This class
+ * provides some useful tools:<ul>
+ * <li>Node</li>
+ * <li>Name</li>
+ * <li>Session ID</li>
+ * <li>Current Stage</li>
+ * <li>Available actions</li>
+ * <li>Default action</li>
+ * </ul><p/>
+ * To implement a new command extend this class and implement all the abstract
+ * methods. When implementing the actions remember that they could be invoked
+ * several times, and that you must use the current stage number to know what to
+ * do.
+ *
+ * @author Gabriel Guardincerri
+ */
+public abstract class LocalCommand extends AdHocCommand {
+
+ /**
+ * The time stamp of first invokation of the command. Used to implement the session timeout.
+ */
+ private long creationDate;
+
+ /**
+ * The unique ID of the execution of the command.
+ */
+ private String sessionID;
+
+ /**
+ * The full JID of the host of the command.
+ */
+ private String ownerJID;
+
+ /**
+ * The number of the current stage.
+ */
+ private int currenStage;
+
+ public LocalCommand() {
+ super();
+ this.creationDate = System.currentTimeMillis();
+ currenStage = -1;
+ }
+
+ /**
+ * The sessionID is an unique identifier of an execution request. This is
+ * automatically handled and should not be called.
+ *
+ * @param sessionID the unique session id of this execution
+ */
+ public void setSessionID(String sessionID) {
+ this.sessionID = sessionID;
+ getData().setSessionID(sessionID);
+ }
+
+ /**
+ * Returns the session ID of this execution.
+ *
+ * @return the unique session id of this execution
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ /**
+ * Sets the JID of the command host. This is automatically handled and should
+ * not be called.
+ *
+ * @param ownerJID the JID of the owner.
+ */
+ public void setOwnerJID(String ownerJID) {
+ this.ownerJID = ownerJID;
+ }
+
+ @Override
+ public String getOwnerJID() {
+ return ownerJID;
+ }
+
+ /**
+ * Returns the date the command was created.
+ *
+ * @return the date the command was created.
+ */
+ public long getCreationDate() {
+ return creationDate;
+ }
+
+ /**
+ * Returns true if the current stage is the last one. If it is then the
+ * execution of some action will complete the execution of the command.
+ * Commands that don't have multiple stages can always return <tt>true</tt>.
+ *
+ * @return true if the command is in the last stage.
+ */
+ public abstract boolean isLastStage();
+
+ /**
+ * Returns true if the specified requester has permission to execute all the
+ * stages of this action. This is checked when the first request is received,
+ * if the permission is grant then the requester will be able to execute
+ * all the stages of the command. It is not checked again during the
+ * execution.
+ *
+ * @param jid the JID to check permissions on.
+ * @return true if the user has permission to execute this action.
+ */
+ public abstract boolean hasPermission(String jid);
+
+ /**
+ * Returns the currently executing stage number. The first stage number is
+ * 0. During the execution of the first action this method will answer 0.
+ *
+ * @return the current stage number.
+ */
+ public int getCurrentStage() {
+ return currenStage;
+ }
+
+ @Override
+ void setData(AdHocCommandData data) {
+ data.setSessionID(sessionID);
+ super.setData(data);
+ }
+
+ /**
+ * Increase the current stage number. This is automatically handled and should
+ * not be called.
+ *
+ */
+ void incrementStage() {
+ currenStage++;
+ }
+
+ /**
+ * Decrease the current stage number. This is automatically handled and should
+ * not be called.
+ *
+ */
+ void decrementStage() {
+ currenStage--;
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/commands/LocalCommandFactory.java b/src/org/jivesoftware/smackx/commands/LocalCommandFactory.java new file mode 100644 index 0000000..83fc455 --- /dev/null +++ b/src/org/jivesoftware/smackx/commands/LocalCommandFactory.java @@ -0,0 +1,43 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2008 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.commands; + +/** + * A factory for creating local commands. It's useful in cases where instantiation + * of a command is more complicated than just using the default constructor. For example, + * when arguments must be passed into the constructor or when using a dependency injection + * framework. When a LocalCommandFactory isn't used, you can provide the AdHocCommandManager + * a Class object instead. For more details, see + * {@link AdHocCommandManager#registerCommand(String, String, LocalCommandFactory)}. + * + * @author Matt Tucker + */ +public interface LocalCommandFactory { + + /** + * Returns an instance of a LocalCommand. + * + * @return a LocalCommand instance. + * @throws InstantiationException if creating an instance failed. + * @throws IllegalAccessException if creating an instance is not allowed. + */ + public LocalCommand getInstance() throws InstantiationException, IllegalAccessException; + +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/commands/RemoteCommand.java b/src/org/jivesoftware/smackx/commands/RemoteCommand.java new file mode 100755 index 0000000..3c2df1d --- /dev/null +++ b/src/org/jivesoftware/smackx/commands/RemoteCommand.java @@ -0,0 +1,201 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2005-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.smackx.commands; + +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.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.packet.AdHocCommandData; + +/** + * Represents a command that is in a remote location. Invoking one of the + * {@link AdHocCommand.Action#execute execute}, {@link AdHocCommand.Action#next next}, + * {@link AdHocCommand.Action#prev prev}, {@link AdHocCommand.Action#cancel cancel} or + * {@link AdHocCommand.Action#complete complete} actions results in executing that + * action in the remote location. In response to that action the internal state + * of the this command instance will change. For example, if the command is a + * single stage command, then invoking the execute action will execute this + * action in the remote location. After that the local instance will have a + * state of "completed" and a form or notes that applies. + * + * @author Gabriel Guardincerri + * + */ +public class RemoteCommand extends AdHocCommand { + + /** + * The connection that is used to execute this command + */ + private Connection connection; + + /** + * The full JID of the command host + */ + private String jid; + + /** + * The session ID of this execution. + */ + private String sessionID; + + + /** + * The number of milliseconds to wait for a response from the server + * The default value is the default packet reply timeout (5000 ms). + */ + private long packetReplyTimeout; + + /** + * Creates a new RemoteCommand that uses an specific connection to execute a + * command identified by <code>node</code> in the host identified by + * <code>jid</code> + * + * @param connection the connection to use for the execution. + * @param node the identifier of the command. + * @param jid the JID of the host. + */ + protected RemoteCommand(Connection connection, String node, String jid) { + super(); + this.connection = connection; + this.jid = jid; + this.setNode(node); + this.packetReplyTimeout = SmackConfiguration.getPacketReplyTimeout(); + } + + @Override + public void cancel() throws XMPPException { + executeAction(Action.cancel, packetReplyTimeout); + } + + @Override + public void complete(Form form) throws XMPPException { + executeAction(Action.complete, form, packetReplyTimeout); + } + + @Override + public void execute() throws XMPPException { + executeAction(Action.execute, packetReplyTimeout); + } + + /** + * Executes the default action of the command with the information provided + * in the Form. This form must be the anwser form of the previous stage. If + * there is a problem executing the command it throws an XMPPException. + * + * @param form the form anwser of the previous stage. + * @throws XMPPException if an error occurs. + */ + public void execute(Form form) throws XMPPException { + executeAction(Action.execute, form, packetReplyTimeout); + } + + @Override + public void next(Form form) throws XMPPException { + executeAction(Action.next, form, packetReplyTimeout); + } + + @Override + public void prev() throws XMPPException { + executeAction(Action.prev, packetReplyTimeout); + } + + private void executeAction(Action action, long packetReplyTimeout) throws XMPPException { + executeAction(action, null, packetReplyTimeout); + } + + /** + * Executes the <code>action</codo> with the <code>form</code>. + * The action could be any of the available actions. The form must + * be the anwser of the previous stage. It can be <tt>null</tt> if it is the first stage. + * + * @param action the action to execute. + * @param form the form with the information. + * @param timeout the amount of time to wait for a reply. + * @throws XMPPException if there is a problem executing the command. + */ + private void executeAction(Action action, Form form, long timeout) throws XMPPException { + // TODO: Check that all the required fields of the form were filled, if + // TODO: not throw the corresponding exeption. This will make a faster response, + // TODO: since the request is stoped before it's sent. + AdHocCommandData data = new AdHocCommandData(); + data.setType(IQ.Type.SET); + data.setTo(getOwnerJID()); + data.setNode(getNode()); + data.setSessionID(sessionID); + data.setAction(action); + + if (form != null) { + data.setForm(form.getDataFormToSend()); + } + + PacketCollector collector = connection.createPacketCollector( + new PacketIDFilter(data.getPacketID())); + + connection.sendPacket(data); + + Packet response = collector.nextResult(timeout); + + // Cancel the collector. + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from server on status set."); + } + if (response.getError() != null) { + throw new XMPPException(response.getError()); + } + + AdHocCommandData responseData = (AdHocCommandData) response; + this.sessionID = responseData.getSessionID(); + super.setData(responseData); + } + + @Override + public String getOwnerJID() { + return jid; + } + + /** + * Returns the number of milliseconds to wait for a respone. The + * {@link SmackConfiguration#getPacketReplyTimeout default} value + * should be adjusted for commands that can take a long time to execute. + * + * @return the number of milliseconds to wait for responses. + */ + public long getPacketReplyTimeout() { + return packetReplyTimeout; + } + + /** + * Returns the number of milliseconds to wait for a respone. The + * {@link SmackConfiguration#getPacketReplyTimeout default} value + * should be adjusted for commands that can take a long time to execute. + * + * @param packetReplyTimeout the number of milliseconds to wait for responses. + */ + public void setPacketReplyTimeout(long packetReplyTimeout) { + this.packetReplyTimeout = packetReplyTimeout; + } +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/debugger/package.html b/src/org/jivesoftware/smackx/debugger/package.html new file mode 100644 index 0000000..8ea20e0 --- /dev/null +++ b/src/org/jivesoftware/smackx/debugger/package.html @@ -0,0 +1 @@ +<body>Smack optional Debuggers.</body>
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java b/src/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java new file mode 100644 index 0000000..1da222e --- /dev/null +++ b/src/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java @@ -0,0 +1,713 @@ +/** + * Copyright 2009 Jonas Ã…dahl. + * Copyright 2011-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.smackx.entitycaps; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketInterceptor; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.filter.NotFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.filter.PacketExtensionFilter; +import org.jivesoftware.smack.util.Base64; +import org.jivesoftware.smack.util.Cache; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.NodeInformationProvider; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.entitycaps.cache.EntityCapsPersistentCache; +import org.jivesoftware.smackx.entitycaps.packet.CapsExtension; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DataForm; +import org.jivesoftware.smackx.packet.DiscoverInfo.Feature; +import org.jivesoftware.smackx.packet.DiscoverInfo.Identity; +import org.jivesoftware.smackx.packet.DiscoverItems.Item; + +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Keeps track of entity capabilities. + * + * @author Florian Schmaus + */ +public class EntityCapsManager { + + public static final String NAMESPACE = "http://jabber.org/protocol/caps"; + public static final String ELEMENT = "c"; + + private static final String ENTITY_NODE = "http://www.igniterealtime.org/projects/smack"; + private static final Map<String, MessageDigest> SUPPORTED_HASHES = new HashMap<String, MessageDigest>(); + + protected static EntityCapsPersistentCache persistentCache; + + private static Map<Connection, EntityCapsManager> instances = Collections + .synchronizedMap(new WeakHashMap<Connection, EntityCapsManager>()); + + /** + * Map of (node + '#" + hash algorithm) to DiscoverInfo data + */ + protected static Map<String, DiscoverInfo> caps = new Cache<String, DiscoverInfo>(1000, -1); + + /** + * Map of Full JID -> DiscoverInfo/null. In case of c2s connection the + * key is formed as user@server/resource (resource is required) In case of + * link-local connection the key is formed as user@host (no resource) In + * case of a server or component the key is formed as domain + */ + protected static Map<String, NodeVerHash> jidCaps = new Cache<String, NodeVerHash>(10000, -1); + + static { + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(Connection connection) { + if (connection instanceof XMPPConnection) + new EntityCapsManager(connection); + } + }); + + try { + MessageDigest sha1MessageDigest = MessageDigest.getInstance("SHA-1"); + SUPPORTED_HASHES.put("sha-1", sha1MessageDigest); + } catch (NoSuchAlgorithmException e) { + // Ignore + } + } + + private WeakReference<Connection> weakRefConnection; + private ServiceDiscoveryManager sdm; + private boolean entityCapsEnabled; + private String currentCapsVersion; + private boolean presenceSend = false; + private Queue<String> lastLocalCapsVersions = new ConcurrentLinkedQueue<String>(); + + /** + * Add DiscoverInfo to the database. + * + * @param nodeVer + * The node and verification String (e.g. + * "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w="). + * @param info + * DiscoverInfo for the specified node. + */ + public static void addDiscoverInfoByNode(String nodeVer, DiscoverInfo info) { + caps.put(nodeVer, info); + + if (persistentCache != null) + persistentCache.addDiscoverInfoByNodePersistent(nodeVer, info); + } + + /** + * Get the Node version (node#ver) of a JID. Returns a String or null if + * EntiyCapsManager does not have any information. + * + * @param user + * the user (Full JID) + * @return the node version (node#ver) or null + */ + public static String getNodeVersionByJid(String jid) { + NodeVerHash nvh = jidCaps.get(jid); + if (nvh != null) { + return nvh.nodeVer; + } else { + return null; + } + } + + public static NodeVerHash getNodeVerHashByJid(String jid) { + return jidCaps.get(jid); + } + + /** + * Get the discover info given a user name. The discover info is returned if + * the user has a node#ver associated with it and the node#ver has a + * discover info associated with it. + * + * @param user + * user name (Full JID) + * @return the discovered info + */ + public static DiscoverInfo getDiscoverInfoByUser(String user) { + NodeVerHash nvh = jidCaps.get(user); + if (nvh == null) + return null; + + return getDiscoveryInfoByNodeVer(nvh.nodeVer); + } + + /** + * Retrieve DiscoverInfo for a specific node. + * + * @param nodeVer + * The node name (e.g. + * "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w="). + * @return The corresponding DiscoverInfo or null if none is known. + */ + public static DiscoverInfo getDiscoveryInfoByNodeVer(String nodeVer) { + DiscoverInfo info = caps.get(nodeVer); + if (info != null) + info = new DiscoverInfo(info); + + return info; + } + + /** + * Set the persistent cache implementation + * + * @param cache + * @throws IOException + */ + public static void setPersistentCache(EntityCapsPersistentCache cache) throws IOException { + if (persistentCache != null) + throw new IllegalStateException("Entity Caps Persistent Cache was already set"); + persistentCache = cache; + persistentCache.replay(); + } + + /** + * Sets the maximum Cache size for the JID to nodeVer Cache + * + * @param maxCacheSize + */ + @SuppressWarnings("rawtypes") + public static void setJidCapsMaxCacheSize(int maxCacheSize) { + ((Cache) jidCaps).setMaxCacheSize(maxCacheSize); + } + + /** + * Sets the maximum Cache size for the nodeVer to DiscoverInfo Cache + * + * @param maxCacheSize + */ + @SuppressWarnings("rawtypes") + public static void setCapsMaxCacheSize(int maxCacheSize) { + ((Cache) caps).setMaxCacheSize(maxCacheSize); + } + + private EntityCapsManager(Connection connection) { + this.weakRefConnection = new WeakReference<Connection>(connection); + this.sdm = ServiceDiscoveryManager.getInstanceFor(connection); + init(); + } + + private void init() { + Connection connection = weakRefConnection.get(); + instances.put(connection, this); + + connection.addConnectionListener(new ConnectionListener() { + public void connectionClosed() { + // Unregister this instance since the connection has been closed + presenceSend = false; + instances.remove(weakRefConnection.get()); + } + + public void connectionClosedOnError(Exception e) { + presenceSend = false; + } + + public void reconnectionFailed(Exception e) { + // ignore + } + + public void reconnectingIn(int seconds) { + // ignore + } + + public void reconnectionSuccessful() { + // ignore + } + }); + + // This calculates the local entity caps version + updateLocalEntityCaps(); + + if (SmackConfiguration.autoEnableEntityCaps()) + enableEntityCaps(); + + PacketFilter packetFilter = new AndFilter(new PacketTypeFilter(Presence.class), new PacketExtensionFilter( + ELEMENT, NAMESPACE)); + connection.addPacketListener(new PacketListener() { + // Listen for remote presence stanzas with the caps extension + // If we receive such a stanza, record the JID and nodeVer + @Override + public void processPacket(Packet packet) { + if (!entityCapsEnabled()) + return; + + CapsExtension ext = (CapsExtension) packet.getExtension(EntityCapsManager.ELEMENT, + EntityCapsManager.NAMESPACE); + + String hash = ext.getHash().toLowerCase(); + if (!SUPPORTED_HASHES.containsKey(hash)) + return; + + String from = packet.getFrom(); + String node = ext.getNode(); + String ver = ext.getVer(); + + jidCaps.put(from, new NodeVerHash(node, ver, hash)); + } + + }, packetFilter); + + packetFilter = new AndFilter(new PacketTypeFilter(Presence.class), new NotFilter(new PacketExtensionFilter( + ELEMENT, NAMESPACE))); + connection.addPacketListener(new PacketListener() { + @Override + public void processPacket(Packet packet) { + // always remove the JID from the map, even if entityCaps are + // disabled + String from = packet.getFrom(); + jidCaps.remove(from); + } + }, packetFilter); + + packetFilter = new PacketTypeFilter(Presence.class); + connection.addPacketSendingListener(new PacketListener() { + @Override + public void processPacket(Packet packet) { + presenceSend = true; + } + }, packetFilter); + + // Intercept presence packages and add caps data when intended. + // XEP-0115 specifies that a client SHOULD include entity capabilities + // with every presence notification it sends. + PacketFilter capsPacketFilter = new PacketTypeFilter(Presence.class); + PacketInterceptor packetInterceptor = new PacketInterceptor() { + public void interceptPacket(Packet packet) { + if (!entityCapsEnabled) + return; + + CapsExtension caps = new CapsExtension(ENTITY_NODE, getCapsVersion(), "sha-1"); + packet.addExtension(caps); + } + }; + connection.addPacketInterceptor(packetInterceptor, capsPacketFilter); + // It's important to do this as last action. Since it changes the + // behavior of the SDM in some ways + sdm.setEntityCapsManager(this); + } + + public static synchronized EntityCapsManager getInstanceFor(Connection connection) { + // For testing purposed forbid EntityCaps for non XMPPConnections + // it may work on BOSH connections too + if (!(connection instanceof XMPPConnection)) + return null; + + if (SUPPORTED_HASHES.size() <= 0) + return null; + + EntityCapsManager entityCapsManager = instances.get(connection); + + if (entityCapsManager == null) { + entityCapsManager = new EntityCapsManager(connection); + } + + return entityCapsManager; + } + + public void enableEntityCaps() { + // Add Entity Capabilities (XEP-0115) feature node. + sdm.addFeature(NAMESPACE); + updateLocalEntityCaps(); + entityCapsEnabled = true; + } + + public void disableEntityCaps() { + entityCapsEnabled = false; + sdm.removeFeature(NAMESPACE); + } + + public boolean entityCapsEnabled() { + return entityCapsEnabled; + } + + /** + * Remove a record telling what entity caps node a user has. + * + * @param user + * the user (Full JID) + */ + public void removeUserCapsNode(String user) { + jidCaps.remove(user); + } + + /** + * Get our own caps version. The version depends on the enabled features. A + * caps version looks like '66/0NaeaBKkwk85efJTGmU47vXI=' + * + * @return our own caps version + */ + public String getCapsVersion() { + return currentCapsVersion; + } + + /** + * Returns the local entity's NodeVer (e.g. + * "http://www.igniterealtime.org/projects/smack/#66/0NaeaBKkwk85efJTGmU47vXI= + * ) + * + * @return + */ + public String getLocalNodeVer() { + return ENTITY_NODE + '#' + getCapsVersion(); + } + + /** + * Returns true if Entity Caps are supported by a given JID + * + * @param jid + * @return + */ + public boolean areEntityCapsSupported(String jid) { + if (jid == null) + return false; + + try { + DiscoverInfo result = sdm.discoverInfo(jid); + return result.containsFeature(NAMESPACE); + } catch (XMPPException e) { + return false; + } + } + + /** + * Returns true if Entity Caps are supported by the local service/server + * + * @return + */ + public boolean areEntityCapsSupportedByServer() { + return areEntityCapsSupported(weakRefConnection.get().getServiceName()); + } + + /** + * Updates the local user Entity Caps information with the data provided + * + * If we are connected and there was already a presence send, another + * presence is send to inform others about your new Entity Caps node string. + * + * @param discoverInfo + * the local users discover info (mostly the service discovery + * features) + * @param identityType + * the local users identity type + * @param identityName + * the local users identity name + * @param extendedInfo + * the local users extended info + */ + public void updateLocalEntityCaps() { + Connection connection = weakRefConnection.get(); + + DiscoverInfo discoverInfo = new DiscoverInfo(); + discoverInfo.setType(IQ.Type.RESULT); + discoverInfo.setNode(getLocalNodeVer()); + if (connection != null) + discoverInfo.setFrom(connection.getUser()); + sdm.addDiscoverInfoTo(discoverInfo); + + currentCapsVersion = generateVerificationString(discoverInfo, "sha-1"); + addDiscoverInfoByNode(ENTITY_NODE + '#' + currentCapsVersion, discoverInfo); + if (lastLocalCapsVersions.size() > 10) { + String oldCapsVersion = lastLocalCapsVersions.poll(); + sdm.removeNodeInformationProvider(ENTITY_NODE + '#' + oldCapsVersion); + } + lastLocalCapsVersions.add(currentCapsVersion); + + caps.put(currentCapsVersion, discoverInfo); + if (connection != null) + jidCaps.put(connection.getUser(), new NodeVerHash(ENTITY_NODE, currentCapsVersion, "sha-1")); + + sdm.setNodeInformationProvider(ENTITY_NODE + '#' + currentCapsVersion, new NodeInformationProvider() { + List<String> features = sdm.getFeaturesList(); + List<Identity> identities = new LinkedList<Identity>(ServiceDiscoveryManager.getIdentities()); + List<PacketExtension> packetExtensions = sdm.getExtendedInfoAsList(); + + @Override + public List<Item> getNodeItems() { + return null; + } + + @Override + public List<String> getNodeFeatures() { + return features; + } + + @Override + public List<Identity> getNodeIdentities() { + return identities; + } + + @Override + public List<PacketExtension> getNodePacketExtensions() { + return packetExtensions; + } + }); + + // Send an empty presence, and let the packet intercepter + // add a <c/> node to it. + // See http://xmpp.org/extensions/xep-0115.html#advertise + // We only send a presence packet if there was already one send + // to respect ConnectionConfiguration.isSendPresence() + if (connection != null && connection.isAuthenticated() && presenceSend) { + Presence presence = new Presence(Presence.Type.available); + connection.sendPacket(presence); + } + } + + /** + * Verify DisoverInfo and Caps Node as defined in XEP-0115 5.4 Processing + * Method + * + * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0115 + * 5.4 Processing Method</a> + * + * @param capsNode + * the caps node (i.e. node#ver) + * @param info + * @return true if it's valid and should be cache, false if not + */ + public static boolean verifyDiscoverInfoVersion(String ver, String hash, DiscoverInfo info) { + // step 3.3 check for duplicate identities + if (info.containsDuplicateIdentities()) + return false; + + // step 3.4 check for duplicate features + if (info.containsDuplicateFeatures()) + return false; + + // step 3.5 check for well-formed packet extensions + if (verifyPacketExtensions(info)) + return false; + + String calculatedVer = generateVerificationString(info, hash); + + if (!ver.equals(calculatedVer)) + return false; + + return true; + } + + /** + * + * @param info + * @return true if the packet extensions is ill-formed + */ + protected static boolean verifyPacketExtensions(DiscoverInfo info) { + List<FormField> foundFormTypes = new LinkedList<FormField>(); + for (Iterator<PacketExtension> i = info.getExtensions().iterator(); i.hasNext();) { + PacketExtension pe = i.next(); + if (pe.getNamespace().equals(Form.NAMESPACE)) { + DataForm df = (DataForm) pe; + for (Iterator<FormField> it = df.getFields(); it.hasNext();) { + FormField f = it.next(); + if (f.getVariable().equals("FORM_TYPE")) { + for (FormField fft : foundFormTypes) { + if (f.equals(fft)) + return true; + } + foundFormTypes.add(f); + } + } + } + } + return false; + } + + /** + * Generates a XEP-115 Verification String + * + * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver">XEP-115 + * Verification String</a> + * + * @param discoverInfo + * @param hash + * the used hash function + * @return The generated verification String or null if the hash is not + * supported + */ + protected static String generateVerificationString(DiscoverInfo discoverInfo, String hash) { + MessageDigest md = SUPPORTED_HASHES.get(hash.toLowerCase()); + if (md == null) + return null; + + DataForm extendedInfo = (DataForm) discoverInfo.getExtension(Form.ELEMENT, Form.NAMESPACE); + + // 1. Initialize an empty string S ('sb' in this method). + StringBuilder sb = new StringBuilder(); // Use StringBuilder as we don't + // need thread-safe StringBuffer + + // 2. Sort the service discovery identities by category and then by + // type and then by xml:lang + // (if it exists), formatted as CATEGORY '/' [TYPE] '/' [LANG] '/' + // [NAME]. Note that each slash is included even if the LANG or + // NAME is not included (in accordance with XEP-0030, the category and + // type MUST be included. + SortedSet<DiscoverInfo.Identity> sortedIdentities = new TreeSet<DiscoverInfo.Identity>(); + + for (Iterator<DiscoverInfo.Identity> it = discoverInfo.getIdentities(); it.hasNext();) + sortedIdentities.add(it.next()); + + // 3. For each identity, append the 'category/type/lang/name' to S, + // followed by the '<' character. + for (Iterator<DiscoverInfo.Identity> it = sortedIdentities.iterator(); it.hasNext();) { + DiscoverInfo.Identity identity = it.next(); + sb.append(identity.getCategory()); + sb.append("/"); + sb.append(identity.getType()); + sb.append("/"); + sb.append(identity.getLanguage() == null ? "" : identity.getLanguage()); + sb.append("/"); + sb.append(identity.getName() == null ? "" : identity.getName()); + sb.append("<"); + } + + // 4. Sort the supported service discovery features. + SortedSet<String> features = new TreeSet<String>(); + for (Iterator<Feature> it = discoverInfo.getFeatures(); it.hasNext();) + features.add(it.next().getVar()); + + // 5. For each feature, append the feature to S, followed by the '<' + // character + for (String f : features) { + sb.append(f); + sb.append("<"); + } + + // only use the data form for calculation is it has a hidden FORM_TYPE + // field + // see XEP-0115 5.4 step 3.6 + if (extendedInfo != null && extendedInfo.hasHiddenFormTypeField()) { + synchronized (extendedInfo) { + // 6. If the service discovery information response includes + // XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e., + // by the XML character data of the <value/> element). + SortedSet<FormField> fs = new TreeSet<FormField>(new Comparator<FormField>() { + public int compare(FormField f1, FormField f2) { + return f1.getVariable().compareTo(f2.getVariable()); + } + }); + + FormField ft = null; + + for (Iterator<FormField> i = extendedInfo.getFields(); i.hasNext();) { + FormField f = i.next(); + if (!f.getVariable().equals("FORM_TYPE")) { + fs.add(f); + } else { + ft = f; + } + } + + // Add FORM_TYPE values + if (ft != null) { + formFieldValuesToCaps(ft.getValues(), sb); + } + + // 7. 3. For each field other than FORM_TYPE: + // 1. Append the value of the "var" attribute, followed by the + // '<' character. + // 2. Sort values by the XML character data of the <value/> + // element. + // 3. For each <value/> element, append the XML character data, + // followed by the '<' character. + for (FormField f : fs) { + sb.append(f.getVariable()); + sb.append("<"); + formFieldValuesToCaps(f.getValues(), sb); + } + } + } + // 8. Ensure that S is encoded according to the UTF-8 encoding (RFC + // 3269). + // 9. Compute the verification string by hashing S using the algorithm + // specified in the 'hash' attribute (e.g., SHA-1 as defined in RFC + // 3174). + // The hashed data MUST be generated with binary output and + // encoded using Base64 as specified in Section 4 of RFC 4648 + // (note: the Base64 output MUST NOT include whitespace and MUST set + // padding bits to zero). + byte[] digest = md.digest(sb.toString().getBytes()); + return Base64.encodeBytes(digest); + } + + private static void formFieldValuesToCaps(Iterator<String> i, StringBuilder sb) { + SortedSet<String> fvs = new TreeSet<String>(); + while (i.hasNext()) { + fvs.add(i.next()); + } + for (String fv : fvs) { + sb.append(fv); + sb.append("<"); + } + } + + public static class NodeVerHash { + private String node; + private String hash; + private String ver; + private String nodeVer; + + NodeVerHash(String node, String ver, String hash) { + this.node = node; + this.ver = ver; + this.hash = hash; + nodeVer = node + "#" + ver; + } + + public String getNodeVer() { + return nodeVer; + } + + public String getNode() { + return node; + } + + public String getHash() { + return hash; + } + + public String getVer() { + return ver; + } + } +} diff --git a/src/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java b/src/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java new file mode 100644 index 0000000..0441043 --- /dev/null +++ b/src/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java @@ -0,0 +1,38 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.entitycaps.cache; + +import java.io.IOException; + +import org.jivesoftware.smackx.packet.DiscoverInfo; + +public interface EntityCapsPersistentCache { + /** + * Add an DiscoverInfo to the persistent Cache + * + * @param node + * @param info + */ + void addDiscoverInfoByNodePersistent(String node, DiscoverInfo info); + + /** + * Replay the Caches data into EntityCapsManager + */ + void replay() throws IOException; + + /** + * Empty the Cache + */ + void emptyCache(); +} diff --git a/src/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java b/src/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java new file mode 100644 index 0000000..0312c7e --- /dev/null +++ b/src/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java @@ -0,0 +1,194 @@ +/** + * Copyright 2011 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.entitycaps.cache; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.util.Base32Encoder; +import org.jivesoftware.smack.util.Base64Encoder; +import org.jivesoftware.smack.util.StringEncoder; +import org.jivesoftware.smackx.entitycaps.EntityCapsManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.provider.DiscoverInfoProvider; +import org.xmlpull.v1.XmlPullParserFactory; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * Simple implementation of an EntityCapsPersistentCache that uses a directory + * to store the Caps information for every known node. Every node is represented + * by an file. + * + * @author Florian Schmaus + * + */ +public class SimpleDirectoryPersistentCache implements EntityCapsPersistentCache { + + private File cacheDir; + private StringEncoder filenameEncoder; + + /** + * Creates a new SimpleDirectoryPersistentCache Object. Make sure that the + * cacheDir exists and that it's an directory. + * <p> + * Default filename encoder {@link Base32Encoder}, as this will work on all + * filesystems, both case sensitive and case insensitive. It does however + * produce longer filenames. + * + * @param cacheDir + */ + public SimpleDirectoryPersistentCache(File cacheDir) { + this(cacheDir, Base32Encoder.getInstance()); + } + + /** + * Creates a new SimpleDirectoryPersistentCache Object. Make sure that the + * cacheDir exists and that it's an directory. + * + * If your cacheDir is case insensitive then make sure to set the + * StringEncoder to {@link Base32Encoder} (which is the default). + * + * @param cacheDir The directory where the cache will be stored. + * @param filenameEncoder Encodes the node string into a filename. + */ + public SimpleDirectoryPersistentCache(File cacheDir, StringEncoder filenameEncoder) { + if (!cacheDir.exists()) + throw new IllegalStateException("Cache directory \"" + cacheDir + "\" does not exist"); + if (!cacheDir.isDirectory()) + throw new IllegalStateException("Cache directory \"" + cacheDir + "\" is not a directory"); + + this.cacheDir = cacheDir; + this.filenameEncoder = filenameEncoder; + } + + @Override + public void addDiscoverInfoByNodePersistent(String node, DiscoverInfo info) { + String filename = filenameEncoder.encode(node); + File nodeFile = new File(cacheDir, filename); + try { + if (nodeFile.createNewFile()) + writeInfoToFile(nodeFile, info); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void replay() throws IOException { + File[] files = cacheDir.listFiles(); + for (File f : files) { + String node = filenameEncoder.decode(f.getName()); + DiscoverInfo info = restoreInfoFromFile(f); + if (info == null) + continue; + + EntityCapsManager.addDiscoverInfoByNode(node, info); + } + } + + public void emptyCache() { + File[] files = cacheDir.listFiles(); + for (File f : files) { + f.delete(); + } + } + + /** + * Writes the DiscoverInfo packet to an file + * + * @param file + * @param info + * @throws IOException + */ + private static void writeInfoToFile(File file, DiscoverInfo info) throws IOException { + DataOutputStream dos = new DataOutputStream(new FileOutputStream(file)); + try { + dos.writeUTF(info.toXML()); + } finally { + dos.close(); + } + } + + /** + * Tries to restore an DiscoverInfo packet from a file. + * + * @param file + * @return + * @throws IOException + */ + private static DiscoverInfo restoreInfoFromFile(File file) throws IOException { + DataInputStream dis = new DataInputStream(new FileInputStream(file)); + String fileContent = null; + String id; + String from; + String to; + + try { + fileContent = dis.readUTF(); + } finally { + dis.close(); + } + if (fileContent == null) + return null; + + Reader reader = new StringReader(fileContent); + XmlPullParser parser; + try { + parser = XmlPullParserFactory.newInstance().newPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(reader); + } catch (XmlPullParserException xppe) { + xppe.printStackTrace(); + return null; + } + + DiscoverInfo iqPacket; + IQProvider provider = new DiscoverInfoProvider(); + + // Parse the IQ, we only need the id + try { + parser.next(); + id = parser.getAttributeValue("", "id"); + from = parser.getAttributeValue("", "from"); + to = parser.getAttributeValue("", "to"); + parser.next(); + } catch (XmlPullParserException e1) { + return null; + } + + try { + iqPacket = (DiscoverInfo) provider.parseIQ(parser); + } catch (Exception e) { + return null; + } + + iqPacket.setPacketID(id); + iqPacket.setFrom(from); + iqPacket.setTo(to); + iqPacket.setType(IQ.Type.RESULT); + return iqPacket; + } +} diff --git a/src/org/jivesoftware/smackx/entitycaps/packet/CapsExtension.java b/src/org/jivesoftware/smackx/entitycaps/packet/CapsExtension.java new file mode 100644 index 0000000..a87c86c --- /dev/null +++ b/src/org/jivesoftware/smackx/entitycaps/packet/CapsExtension.java @@ -0,0 +1,83 @@ +/** + * Copyright 2009 Jonas Ã…dahl. + * Copyright 2011-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.smackx.entitycaps.packet; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.entitycaps.EntityCapsManager; + +public class CapsExtension implements PacketExtension { + + private String node, ver, hash; + + public CapsExtension() { + } + + public CapsExtension(String node, String version, String hash) { + this.node = node; + this.ver = version; + this.hash = hash; + } + + public String getElementName() { + return EntityCapsManager.ELEMENT; + } + + public String getNamespace() { + return EntityCapsManager.NAMESPACE; + } + + public String getNode() { + return node; + } + + public void setNode(String node) { + this.node = node; + } + + public String getVer() { + return ver; + } + + public void setVer(String ver) { + this.ver = ver; + } + + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } + + /* + * <c xmlns='http://jabber.org/protocol/caps' + * hash='sha-1' + * node='http://code.google.com/p/exodus' + * ver='QgayPKawpkPSDYmwT/WM94uAlu0='/> + * + */ + public String toXML() { + String xml = "<" + EntityCapsManager.ELEMENT + " xmlns=\"" + EntityCapsManager.NAMESPACE + "\" " + + "hash=\"" + hash + "\" " + + "node=\"" + node + "\" " + + "ver=\"" + ver + "\"/>"; + + return xml; + } +} diff --git a/src/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java b/src/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java new file mode 100644 index 0000000..a112cd5 --- /dev/null +++ b/src/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java @@ -0,0 +1,60 @@ +/** + * Copyright 2009 Jonas Ã…dahl. + * Copyright 2011-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.smackx.entitycaps.provider; + +import java.io.IOException; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.entitycaps.EntityCapsManager; +import org.jivesoftware.smackx.entitycaps.packet.CapsExtension; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +public class CapsExtensionProvider implements PacketExtensionProvider { + + public PacketExtension parseExtension(XmlPullParser parser) throws XmlPullParserException, IOException, + XMPPException { + String hash = null; + String version = null; + String node = null; + if (parser.getEventType() == XmlPullParser.START_TAG + && parser.getName().equalsIgnoreCase(EntityCapsManager.ELEMENT)) { + hash = parser.getAttributeValue(null, "hash"); + version = parser.getAttributeValue(null, "ver"); + node = parser.getAttributeValue(null, "node"); + } else { + throw new XMPPException("Malformed Caps element"); + } + + parser.next(); + + if (!(parser.getEventType() == XmlPullParser.END_TAG + && parser.getName().equalsIgnoreCase(EntityCapsManager.ELEMENT))) { + throw new XMPPException("Malformed nested Caps element"); + } + + if (hash != null && version != null && node != null) { + return new CapsExtension(node, version, hash); + } else { + throw new XMPPException("Caps elment with missing attributes"); + } + } +} diff --git a/src/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java b/src/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java new file mode 100644 index 0000000..22b5b1d --- /dev/null +++ b/src/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java @@ -0,0 +1,186 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.filetransfer; + +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.OrFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.packet.StreamInitiation; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.*; +import java.util.List; +import java.util.ArrayList; + + +/** + * The fault tolerant negotiator takes two stream negotiators, the primary and the secondary + * negotiator. If the primary negotiator fails during the stream negotiaton process, the second + * negotiator is used. + */ +public class FaultTolerantNegotiator extends StreamNegotiator { + + private StreamNegotiator primaryNegotiator; + private StreamNegotiator secondaryNegotiator; + private Connection connection; + private PacketFilter primaryFilter; + private PacketFilter secondaryFilter; + + public FaultTolerantNegotiator(Connection connection, StreamNegotiator primary, + StreamNegotiator secondary) { + this.primaryNegotiator = primary; + this.secondaryNegotiator = secondary; + this.connection = connection; + } + + public PacketFilter getInitiationPacketFilter(String from, String streamID) { + if (primaryFilter == null || secondaryFilter == null) { + primaryFilter = primaryNegotiator.getInitiationPacketFilter(from, streamID); + secondaryFilter = secondaryNegotiator.getInitiationPacketFilter(from, streamID); + } + return new OrFilter(primaryFilter, secondaryFilter); + } + + InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException { + throw new UnsupportedOperationException("Negotiation only handled by create incoming " + + "stream method."); + } + + final Packet initiateIncomingStream(Connection connection, StreamInitiation initiation) { + throw new UnsupportedOperationException("Initiation handled by createIncomingStream " + + "method"); + } + + public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException { + PacketCollector collector = connection.createPacketCollector( + getInitiationPacketFilter(initiation.getFrom(), initiation.getSessionID())); + + connection.sendPacket(super.createInitiationAccept(initiation, getNamespaces())); + + ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(2); + CompletionService<InputStream> service + = new ExecutorCompletionService<InputStream>(threadPoolExecutor); + List<Future<InputStream>> futures = new ArrayList<Future<InputStream>>(); + InputStream stream = null; + XMPPException exception = null; + try { + futures.add(service.submit(new NegotiatorService(collector))); + futures.add(service.submit(new NegotiatorService(collector))); + + int i = 0; + while (stream == null && i < futures.size()) { + Future<InputStream> future; + try { + i++; + future = service.poll(10, TimeUnit.SECONDS); + } + catch (InterruptedException e) { + continue; + } + + if (future == null) { + continue; + } + + try { + stream = future.get(); + } + catch (InterruptedException e) { + /* Do Nothing */ + } + catch (ExecutionException e) { + exception = new XMPPException(e.getCause()); + } + } + } + finally { + for (Future<InputStream> future : futures) { + future.cancel(true); + } + collector.cancel(); + threadPoolExecutor.shutdownNow(); + } + if (stream == null) { + if (exception != null) { + throw exception; + } + else { + throw new XMPPException("File transfer negotiation failed."); + } + } + + return stream; + } + + private StreamNegotiator determineNegotiator(Packet streamInitiation) { + return primaryFilter.accept(streamInitiation) ? primaryNegotiator : secondaryNegotiator; + } + + public OutputStream createOutgoingStream(String streamID, String initiator, String target) + throws XMPPException { + OutputStream stream; + try { + stream = primaryNegotiator.createOutgoingStream(streamID, initiator, target); + } + catch (XMPPException ex) { + stream = secondaryNegotiator.createOutgoingStream(streamID, initiator, target); + } + + return stream; + } + + public String[] getNamespaces() { + String[] primary = primaryNegotiator.getNamespaces(); + String[] secondary = secondaryNegotiator.getNamespaces(); + + String[] namespaces = new String[primary.length + secondary.length]; + System.arraycopy(primary, 0, namespaces, 0, primary.length); + System.arraycopy(secondary, 0, namespaces, primary.length, secondary.length); + + return namespaces; + } + + public void cleanup() { + } + + private class NegotiatorService implements Callable<InputStream> { + + private PacketCollector collector; + + NegotiatorService(PacketCollector collector) { + this.collector = collector; + } + + public InputStream call() throws Exception { + Packet streamInitiation = collector.nextResult( + SmackConfiguration.getPacketReplyTimeout() * 2); + if (streamInitiation == null) { + throw new XMPPException("No response from remote client"); + } + StreamNegotiator negotiator = determineNegotiator(streamInitiation); + return negotiator.negotiateIncomingStream(streamInitiation); + } + } +} diff --git a/src/org/jivesoftware/smackx/filetransfer/FileTransfer.java b/src/org/jivesoftware/smackx/filetransfer/FileTransfer.java new file mode 100644 index 0000000..b840fd5 --- /dev/null +++ b/src/org/jivesoftware/smackx/filetransfer/FileTransfer.java @@ -0,0 +1,380 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.filetransfer;
+
+import org.jivesoftware.smack.XMPPException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Contains the generic file information and progress related to a particular
+ * file transfer.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+public abstract class FileTransfer {
+
+ private String fileName;
+
+ private String filePath;
+
+ private long fileSize;
+
+ private String peer;
+
+ private Status status = Status.initial;
+
+ private final Object statusMonitor = new Object();
+
+ protected FileTransferNegotiator negotiator;
+
+ protected String streamID;
+
+ protected long amountWritten = -1;
+
+ private Error error;
+
+ private Exception exception;
+
+ /**
+ * Buffer size between input and output
+ */
+ private static final int BUFFER_SIZE = 8192;
+
+ protected FileTransfer(String peer, String streamID,
+ FileTransferNegotiator negotiator) {
+ this.peer = peer;
+ this.streamID = streamID;
+ this.negotiator = negotiator;
+ }
+
+ protected void setFileInfo(String fileName, long fileSize) {
+ this.fileName = fileName;
+ this.fileSize = fileSize;
+ }
+
+ protected void setFileInfo(String path, String fileName, long fileSize) {
+ this.filePath = path;
+ this.fileName = fileName;
+ this.fileSize = fileSize;
+ }
+
+ /**
+ * Returns the size of the file being transfered.
+ *
+ * @return Returns the size of the file being transfered.
+ */
+ public long getFileSize() {
+ return fileSize;
+ }
+
+ /**
+ * Returns the name of the file being transfered.
+ *
+ * @return Returns the name of the file being transfered.
+ */
+ public String getFileName() {
+ return fileName;
+ }
+
+ /**
+ * Returns the local path of the file.
+ *
+ * @return Returns the local path of the file.
+ */
+ public String getFilePath() {
+ return filePath;
+ }
+
+ /**
+ * Returns the JID of the peer for this file transfer.
+ *
+ * @return Returns the JID of the peer for this file transfer.
+ */
+ public String getPeer() {
+ return peer;
+ }
+
+ /**
+ * Returns the progress of the file transfer as a number between 0 and 1.
+ *
+ * @return Returns the progress of the file transfer as a number between 0
+ * and 1.
+ */
+ public double getProgress() {
+ if (amountWritten <= 0 || fileSize <= 0) {
+ return 0;
+ }
+ return (double) amountWritten / (double) fileSize;
+ }
+
+ /**
+ * Returns true if the transfer has been cancelled, if it has stopped because
+ * of a an error, or the transfer completed successfully.
+ *
+ * @return Returns true if the transfer has been cancelled, if it has stopped
+ * because of a an error, or the transfer completed successfully.
+ */
+ public boolean isDone() {
+ return status == Status.cancelled || status == Status.error
+ || status == Status.complete || status == Status.refused;
+ }
+
+ /**
+ * Returns the current status of the file transfer.
+ *
+ * @return Returns the current status of the file transfer.
+ */
+ public Status getStatus() {
+ return status;
+ }
+
+ protected void setError(Error type) {
+ this.error = type;
+ }
+
+ /**
+ * When {@link #getStatus()} returns that there was an {@link Status#error}
+ * during the transfer, the type of error can be retrieved through this
+ * method.
+ *
+ * @return Returns the type of error that occurred if one has occurred.
+ */
+ public Error getError() {
+ return error;
+ }
+
+ /**
+ * If an exception occurs asynchronously it will be stored for later
+ * retrieval. If there is an error there maybe an exception set.
+ *
+ * @return The exception that occurred or null if there was no exception.
+ * @see #getError()
+ */
+ public Exception getException() {
+ return exception;
+ }
+
+ public String getStreamID() {
+ return streamID;
+ }
+
+ /**
+ * Cancels the file transfer.
+ */
+ public abstract void cancel();
+
+ protected void setException(Exception exception) {
+ this.exception = exception;
+ }
+
+ protected void setStatus(Status status) {
+ synchronized (statusMonitor) {
+ this.status = status;
+ }
+ }
+
+ protected boolean updateStatus(Status oldStatus, Status newStatus) {
+ synchronized (statusMonitor) {
+ if (oldStatus != status) {
+ return false;
+ }
+ status = newStatus;
+ return true;
+ }
+ }
+
+ protected void writeToStream(final InputStream in, final OutputStream out)
+ throws XMPPException
+ {
+ final byte[] b = new byte[BUFFER_SIZE];
+ int count = 0;
+ amountWritten = 0;
+
+ do {
+ // write to the output stream
+ try {
+ out.write(b, 0, count);
+ } catch (IOException e) {
+ throw new XMPPException("error writing to output stream", e);
+ }
+
+ amountWritten += count;
+
+ // read more bytes from the input stream
+ try {
+ count = in.read(b);
+ } catch (IOException e) {
+ throw new XMPPException("error reading from input stream", e);
+ }
+ } while (count != -1 && !getStatus().equals(Status.cancelled));
+
+ // the connection was likely terminated abrubtly if these are not equal
+ if (!getStatus().equals(Status.cancelled) && getError() == Error.none
+ && amountWritten != fileSize) {
+ setStatus(Status.error);
+ this.error = Error.connection;
+ }
+ }
+
+ /**
+ * A class to represent the current status of the file transfer.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+ public enum Status {
+
+ /**
+ * An error occurred during the transfer.
+ *
+ * @see FileTransfer#getError()
+ */
+ error("Error"),
+
+ /**
+ * The initial status of the file transfer.
+ */
+ initial("Initial"),
+
+ /**
+ * The file transfer is being negotiated with the peer. The party
+ * Receiving the file has the option to accept or refuse a file transfer
+ * request. If they accept, then the process of stream negotiation will
+ * begin. If they refuse the file will not be transfered.
+ *
+ * @see #negotiating_stream
+ */
+ negotiating_transfer("Negotiating Transfer"),
+
+ /**
+ * The peer has refused the file transfer request halting the file
+ * transfer negotiation process.
+ */
+ refused("Refused"),
+
+ /**
+ * The stream to transfer the file is being negotiated over the chosen
+ * stream type. After the stream negotiating process is complete the
+ * status becomes negotiated.
+ *
+ * @see #negotiated
+ */
+ negotiating_stream("Negotiating Stream"),
+
+ /**
+ * After the stream negotiation has completed the intermediate state
+ * between the time when the negotiation is finished and the actual
+ * transfer begins.
+ */
+ negotiated("Negotiated"),
+
+ /**
+ * The transfer is in progress.
+ *
+ * @see FileTransfer#getProgress()
+ */
+ in_progress("In Progress"),
+
+ /**
+ * The transfer has completed successfully.
+ */
+ complete("Complete"),
+
+ /**
+ * The file transfer was cancelled
+ */
+ cancelled("Cancelled");
+
+ private String status;
+
+ private Status(String status) {
+ this.status = status;
+ }
+
+ public String toString() {
+ return status;
+ }
+ }
+
+ /**
+ * Return the length of bytes written out to the stream.
+ * @return the amount in bytes written out.
+ */
+ public long getAmountWritten(){
+ return amountWritten;
+ }
+
+ public enum Error {
+ /**
+ * No error
+ */
+ none("No error"),
+
+ /**
+ * The peer did not find any of the provided stream mechanisms
+ * acceptable.
+ */
+ not_acceptable("The peer did not find any of the provided stream mechanisms acceptable."),
+
+ /**
+ * The provided file to transfer does not exist or could not be read.
+ */
+ bad_file("The provided file to transfer does not exist or could not be read."),
+
+ /**
+ * The remote user did not respond or the connection timed out.
+ */
+ no_response("The remote user did not respond or the connection timed out."),
+
+ /**
+ * An error occurred over the socket connected to send the file.
+ */
+ connection("An error occured over the socket connected to send the file."),
+
+ /**
+ * An error occurred while sending or receiving the file
+ */
+ stream("An error occured while sending or recieving the file.");
+
+ private final String msg;
+
+ private Error(String msg) {
+ this.msg = msg;
+ }
+
+ /**
+ * Returns a String representation of this error.
+ *
+ * @return Returns a String representation of this error.
+ */
+ public String getMessage() {
+ return msg;
+ }
+
+ public String toString() {
+ return msg;
+ }
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/filetransfer/FileTransferListener.java b/src/org/jivesoftware/smackx/filetransfer/FileTransferListener.java new file mode 100644 index 0000000..8e07543 --- /dev/null +++ b/src/org/jivesoftware/smackx/filetransfer/FileTransferListener.java @@ -0,0 +1,36 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.filetransfer;
+
+/**
+ * File transfers can cause several events to be raised. These events can be
+ * monitored through this interface.
+ *
+ * @author Alexander Wenckus
+ */
+public interface FileTransferListener {
+ /**
+ * A request to send a file has been recieved from another user.
+ *
+ * @param request
+ * The request from the other user.
+ */
+ public void fileTransferRequest(final FileTransferRequest request);
+}
diff --git a/src/org/jivesoftware/smackx/filetransfer/FileTransferManager.java b/src/org/jivesoftware/smackx/filetransfer/FileTransferManager.java new file mode 100644 index 0000000..6e413fa --- /dev/null +++ b/src/org/jivesoftware/smackx/filetransfer/FileTransferManager.java @@ -0,0 +1,182 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.filetransfer;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.IQTypeFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The file transfer manager class handles the sending and recieving of files.
+ * To send a file invoke the {@link #createOutgoingFileTransfer(String)} method.
+ * <p>
+ * And to recieve a file add a file transfer listener to the manager. The
+ * listener will notify you when there is a new file transfer request. To create
+ * the {@link IncomingFileTransfer} object accept the transfer, or, if the
+ * transfer is not desirable reject it.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+public class FileTransferManager {
+
+ private final FileTransferNegotiator fileTransferNegotiator;
+
+ private List<FileTransferListener> listeners;
+
+ private Connection connection;
+
+ /**
+ * Creates a file transfer manager to initiate and receive file transfers.
+ *
+ * @param connection
+ * The Connection that the file transfers will use.
+ */
+ public FileTransferManager(Connection connection) {
+ this.connection = connection;
+ this.fileTransferNegotiator = FileTransferNegotiator
+ .getInstanceFor(connection);
+ }
+
+ /**
+ * Add a file transfer listener to listen to incoming file transfer
+ * requests.
+ *
+ * @param li
+ * The listener
+ * @see #removeFileTransferListener(FileTransferListener)
+ * @see FileTransferListener
+ */
+ public void addFileTransferListener(final FileTransferListener li) {
+ if (listeners == null) {
+ initListeners();
+ }
+ synchronized (this.listeners) {
+ listeners.add(li);
+ }
+ }
+
+ private void initListeners() {
+ listeners = new ArrayList<FileTransferListener>();
+
+ connection.addPacketListener(new PacketListener() {
+ public void processPacket(Packet packet) {
+ fireNewRequest((StreamInitiation) packet);
+ }
+ }, new AndFilter(new PacketTypeFilter(StreamInitiation.class),
+ new IQTypeFilter(IQ.Type.SET)));
+ }
+
+ protected void fireNewRequest(StreamInitiation initiation) {
+ FileTransferListener[] listeners = null;
+ synchronized (this.listeners) {
+ listeners = new FileTransferListener[this.listeners.size()];
+ this.listeners.toArray(listeners);
+ }
+ FileTransferRequest request = new FileTransferRequest(this, initiation);
+ for (int i = 0; i < listeners.length; i++) {
+ listeners[i].fileTransferRequest(request);
+ }
+ }
+
+ /**
+ * Removes a file transfer listener.
+ *
+ * @param li
+ * The file transfer listener to be removed
+ * @see FileTransferListener
+ */
+ public void removeFileTransferListener(final FileTransferListener li) {
+ if (listeners == null) {
+ return;
+ }
+ synchronized (this.listeners) {
+ listeners.remove(li);
+ }
+ }
+
+ /**
+ * Creates an OutgoingFileTransfer to send a file to another user.
+ *
+ * @param userID
+ * The fully qualified jabber ID (i.e. full JID) with resource of the user to
+ * send the file to.
+ * @return The send file object on which the negotiated transfer can be run.
+ * @exception IllegalArgumentException if userID is null or not a full JID
+ */
+ public OutgoingFileTransfer createOutgoingFileTransfer(String userID) {
+ if (userID == null) {
+ throw new IllegalArgumentException("userID was null");
+ }
+ // We need to create outgoing file transfers with a full JID since this method will later
+ // use XEP-0095 to negotiate the stream. This is done with IQ stanzas that need to be addressed to a full JID
+ // in order to reach an client entity.
+ else if (!StringUtils.isFullJID(userID)) {
+ throw new IllegalArgumentException("The provided user id was not a full JID (i.e. with resource part)");
+ }
+
+ return new OutgoingFileTransfer(connection.getUser(), userID,
+ fileTransferNegotiator.getNextStreamID(),
+ fileTransferNegotiator);
+ }
+
+ /**
+ * When the file transfer request is acceptable, this method should be
+ * invoked. It will create an IncomingFileTransfer which allows the
+ * transmission of the file to procede.
+ *
+ * @param request
+ * The remote request that is being accepted.
+ * @return The IncomingFileTransfer which manages the download of the file
+ * from the transfer initiator.
+ */
+ protected IncomingFileTransfer createIncomingFileTransfer(
+ FileTransferRequest request) {
+ if (request == null) {
+ throw new NullPointerException("RecieveRequest cannot be null");
+ }
+
+ IncomingFileTransfer transfer = new IncomingFileTransfer(request,
+ fileTransferNegotiator);
+ transfer.setFileInfo(request.getFileName(), request.getFileSize());
+
+ return transfer;
+ }
+
+ protected void rejectIncomingFileTransfer(FileTransferRequest request) {
+ StreamInitiation initiation = request.getStreamInitiation();
+
+ IQ rejection = FileTransferNegotiator.createIQ(
+ initiation.getPacketID(), initiation.getFrom(), initiation
+ .getTo(), IQ.Type.ERROR);
+ rejection.setError(new XMPPError(XMPPError.Condition.no_acceptable));
+ connection.sendPacket(rejection);
+ }
+}
diff --git a/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java b/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java new file mode 100644 index 0000000..d1fb7bf --- /dev/null +++ b/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java @@ -0,0 +1,485 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.filetransfer;
+
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+/**
+ * Manages the negotiation of file transfers according to JEP-0096. If a file is
+ * being sent the remote user chooses the type of stream under which the file
+ * will be sent.
+ *
+ * @author Alexander Wenckus
+ * @see <a href="http://xmpp.org/extensions/xep-0096.html">XEP-0096: SI File Transfer</a>
+ */
+public class FileTransferNegotiator {
+
+ // Static
+
+ private static final String[] NAMESPACE = {
+ "http://jabber.org/protocol/si/profile/file-transfer",
+ "http://jabber.org/protocol/si"};
+
+ private static final Map<Connection, FileTransferNegotiator> transferObject =
+ new ConcurrentHashMap<Connection, FileTransferNegotiator>();
+
+ private static final String STREAM_INIT_PREFIX = "jsi_";
+
+ protected static final String STREAM_DATA_FIELD_NAME = "stream-method";
+
+ private static final Random randomGenerator = new Random();
+
+ /**
+ * A static variable to use only offer IBB for file transfer. It is generally recommend to only
+ * set this variable to true for testing purposes as IBB is the backup file transfer method
+ * and shouldn't be used as the only transfer method in production systems.
+ */
+ public static boolean IBB_ONLY = (System.getProperty("ibb") != null);//true;
+
+ /**
+ * Returns the file transfer negotiator related to a particular connection.
+ * When this class is requested on a particular connection the file transfer
+ * service is automatically enabled.
+ *
+ * @param connection The connection for which the transfer manager is desired
+ * @return The IMFileTransferManager
+ */
+ public static FileTransferNegotiator getInstanceFor(
+ final Connection connection) {
+ if (connection == null) {
+ throw new IllegalArgumentException("Connection cannot be null");
+ }
+ if (!connection.isConnected()) {
+ return null;
+ }
+
+ if (transferObject.containsKey(connection)) {
+ return transferObject.get(connection);
+ }
+ else {
+ FileTransferNegotiator transfer = new FileTransferNegotiator(
+ connection);
+ setServiceEnabled(connection, true);
+ transferObject.put(connection, transfer);
+ return transfer;
+ }
+ }
+
+ /**
+ * Enable the Jabber services related to file transfer on the particular
+ * connection.
+ *
+ * @param connection The connection on which to enable or disable the services.
+ * @param isEnabled True to enable, false to disable.
+ */
+ public static void setServiceEnabled(final Connection connection,
+ final boolean isEnabled) {
+ ServiceDiscoveryManager manager = ServiceDiscoveryManager
+ .getInstanceFor(connection);
+
+ List<String> namespaces = new ArrayList<String>();
+ namespaces.addAll(Arrays.asList(NAMESPACE));
+ namespaces.add(InBandBytestreamManager.NAMESPACE);
+ if (!IBB_ONLY) {
+ namespaces.add(Socks5BytestreamManager.NAMESPACE);
+ }
+
+ for (String namespace : namespaces) {
+ if (isEnabled) {
+ if (!manager.includesFeature(namespace)) {
+ manager.addFeature(namespace);
+ }
+ } else {
+ manager.removeFeature(namespace);
+ }
+ }
+
+ }
+
+ /**
+ * Checks to see if all file transfer related services are enabled on the
+ * connection.
+ *
+ * @param connection The connection to check
+ * @return True if all related services are enabled, false if they are not.
+ */
+ public static boolean isServiceEnabled(final Connection connection) {
+ ServiceDiscoveryManager manager = ServiceDiscoveryManager
+ .getInstanceFor(connection);
+
+ List<String> namespaces = new ArrayList<String>();
+ namespaces.addAll(Arrays.asList(NAMESPACE));
+ namespaces.add(InBandBytestreamManager.NAMESPACE);
+ if (!IBB_ONLY) {
+ namespaces.add(Socks5BytestreamManager.NAMESPACE);
+ }
+
+ for (String namespace : namespaces) {
+ if (!manager.includesFeature(namespace)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * A convenience method to create an IQ packet.
+ *
+ * @param ID The packet ID of the
+ * @param to To whom the packet is addressed.
+ * @param from From whom the packet is sent.
+ * @param type The IQ type of the packet.
+ * @return The created IQ packet.
+ */
+ public static IQ createIQ(final String ID, final String to,
+ final String from, final IQ.Type type) {
+ IQ iqPacket = new IQ() {
+ public String getChildElementXML() {
+ return null;
+ }
+ };
+ iqPacket.setPacketID(ID);
+ iqPacket.setTo(to);
+ iqPacket.setFrom(from);
+ iqPacket.setType(type);
+
+ return iqPacket;
+ }
+
+ /**
+ * Returns a collection of the supported transfer protocols.
+ *
+ * @return Returns a collection of the supported transfer protocols.
+ */
+ public static Collection<String> getSupportedProtocols() {
+ List<String> protocols = new ArrayList<String>();
+ protocols.add(InBandBytestreamManager.NAMESPACE);
+ if (!IBB_ONLY) {
+ protocols.add(Socks5BytestreamManager.NAMESPACE);
+ }
+ return Collections.unmodifiableList(protocols);
+ }
+
+ // non-static
+
+ private final Connection connection;
+
+ private final StreamNegotiator byteStreamTransferManager;
+
+ private final StreamNegotiator inbandTransferManager;
+
+ private FileTransferNegotiator(final Connection connection) {
+ configureConnection(connection);
+
+ this.connection = connection;
+ byteStreamTransferManager = new Socks5TransferNegotiator(connection);
+ inbandTransferManager = new IBBTransferNegotiator(connection);
+ }
+
+ private void configureConnection(final Connection connection) {
+ connection.addConnectionListener(new ConnectionListener() {
+ public void connectionClosed() {
+ cleanup(connection);
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ cleanup(connection);
+ }
+
+ public void reconnectionFailed(Exception e) {
+ // ignore
+ }
+
+ public void reconnectionSuccessful() {
+ // ignore
+ }
+
+ public void reconnectingIn(int seconds) {
+ // ignore
+ }
+ });
+ }
+
+ private void cleanup(final Connection connection) {
+ if (transferObject.remove(connection) != null) {
+ inbandTransferManager.cleanup();
+ }
+ }
+
+ /**
+ * Selects an appropriate stream negotiator after examining the incoming file transfer request.
+ *
+ * @param request The related file transfer request.
+ * @return The file transfer object that handles the transfer
+ * @throws XMPPException If there are either no stream methods contained in the packet, or
+ * there is not an appropriate stream method.
+ */
+ public StreamNegotiator selectStreamNegotiator(
+ FileTransferRequest request) throws XMPPException {
+ StreamInitiation si = request.getStreamInitiation();
+ FormField streamMethodField = getStreamMethodField(si
+ .getFeatureNegotiationForm());
+
+ if (streamMethodField == null) {
+ String errorMessage = "No stream methods contained in packet.";
+ XMPPError error = new XMPPError(XMPPError.Condition.bad_request, errorMessage);
+ IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),
+ IQ.Type.ERROR);
+ iqPacket.setError(error);
+ connection.sendPacket(iqPacket);
+ throw new XMPPException(errorMessage, error);
+ }
+
+ // select the appropriate protocol
+
+ StreamNegotiator selectedStreamNegotiator;
+ try {
+ selectedStreamNegotiator = getNegotiator(streamMethodField);
+ }
+ catch (XMPPException e) {
+ IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),
+ IQ.Type.ERROR);
+ iqPacket.setError(e.getXMPPError());
+ connection.sendPacket(iqPacket);
+ throw e;
+ }
+
+ // return the appropriate negotiator
+
+ return selectedStreamNegotiator;
+ }
+
+ private FormField getStreamMethodField(DataForm form) {
+ FormField field = null;
+ for (Iterator<FormField> it = form.getFields(); it.hasNext();) {
+ field = it.next();
+ if (field.getVariable().equals(STREAM_DATA_FIELD_NAME)) {
+ break;
+ }
+ field = null;
+ }
+ return field;
+ }
+
+ private StreamNegotiator getNegotiator(final FormField field)
+ throws XMPPException {
+ String variable;
+ boolean isByteStream = false;
+ boolean isIBB = false;
+ for (Iterator<FormField.Option> it = field.getOptions(); it.hasNext();) {
+ variable = it.next().getValue();
+ if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) {
+ isByteStream = true;
+ }
+ else if (variable.equals(InBandBytestreamManager.NAMESPACE)) {
+ isIBB = true;
+ }
+ }
+
+ if (!isByteStream && !isIBB) {
+ XMPPError error = new XMPPError(XMPPError.Condition.bad_request,
+ "No acceptable transfer mechanism");
+ throw new XMPPException(error.getMessage(), error);
+ }
+
+ //if (isByteStream && isIBB && field.getType().equals(FormField.TYPE_LIST_MULTI)) {
+ if (isByteStream && isIBB) {
+ return new FaultTolerantNegotiator(connection,
+ byteStreamTransferManager,
+ inbandTransferManager);
+ }
+ else if (isByteStream) {
+ return byteStreamTransferManager;
+ }
+ else {
+ return inbandTransferManager;
+ }
+ }
+
+ /**
+ * Reject a stream initiation request from a remote user.
+ *
+ * @param si The Stream Initiation request to reject.
+ */
+ public void rejectStream(final StreamInitiation si) {
+ XMPPError error = new XMPPError(XMPPError.Condition.forbidden, "Offer Declined");
+ IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),
+ IQ.Type.ERROR);
+ iqPacket.setError(error);
+ connection.sendPacket(iqPacket);
+ }
+
+ /**
+ * Returns a new, unique, stream ID to identify a file transfer.
+ *
+ * @return Returns a new, unique, stream ID to identify a file transfer.
+ */
+ public String getNextStreamID() {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append(STREAM_INIT_PREFIX);
+ buffer.append(Math.abs(randomGenerator.nextLong()));
+
+ return buffer.toString();
+ }
+
+ /**
+ * Send a request to another user to send them a file. The other user has
+ * the option of, accepting, rejecting, or not responding to a received file
+ * transfer request.
+ * <p/>
+ * If they accept, the packet will contain the other user's chosen stream
+ * type to send the file across. The two choices this implementation
+ * provides to the other user for file transfer are <a
+ * href="http://www.jabber.org/jeps/jep-0065.html">SOCKS5 Bytestreams</a>,
+ * which is the preferred method of transfer, and <a
+ * href="http://www.jabber.org/jeps/jep-0047.html">In-Band Bytestreams</a>,
+ * which is the fallback mechanism.
+ * <p/>
+ * The other user may choose to decline the file request if they do not
+ * desire the file, their client does not support JEP-0096, or if there are
+ * no acceptable means to transfer the file.
+ * <p/>
+ * Finally, if the other user does not respond this method will return null
+ * after the specified timeout.
+ *
+ * @param userID The userID of the user to whom the file will be sent.
+ * @param streamID The unique identifier for this file transfer.
+ * @param fileName The name of this file. Preferably it should include an
+ * extension as it is used to determine what type of file it is.
+ * @param size The size, in bytes, of the file.
+ * @param desc A description of the file.
+ * @param responseTimeout The amount of time, in milliseconds, to wait for the remote
+ * user to respond. If they do not respond in time, this
+ * @return Returns the stream negotiator selected by the peer.
+ * @throws XMPPException Thrown if there is an error negotiating the file transfer.
+ */
+ public StreamNegotiator negotiateOutgoingTransfer(final String userID,
+ final String streamID, final String fileName, final long size,
+ final String desc, int responseTimeout) throws XMPPException {
+ StreamInitiation si = new StreamInitiation();
+ si.setSesssionID(streamID);
+ si.setMimeType(URLConnection.guessContentTypeFromName(fileName));
+
+ StreamInitiation.File siFile = new StreamInitiation.File(fileName, size);
+ siFile.setDesc(desc);
+ si.setFile(siFile);
+
+ si.setFeatureNegotiationForm(createDefaultInitiationForm());
+
+ si.setFrom(connection.getUser());
+ si.setTo(userID);
+ si.setType(IQ.Type.SET);
+
+ PacketCollector collector = connection
+ .createPacketCollector(new PacketIDFilter(si.getPacketID()));
+ connection.sendPacket(si);
+ Packet siResponse = collector.nextResult(responseTimeout);
+ collector.cancel();
+
+ if (siResponse instanceof IQ) {
+ IQ iqResponse = (IQ) siResponse;
+ if (iqResponse.getType().equals(IQ.Type.RESULT)) {
+ StreamInitiation response = (StreamInitiation) siResponse;
+ return getOutgoingNegotiator(getStreamMethodField(response
+ .getFeatureNegotiationForm()));
+
+ }
+ else if (iqResponse.getType().equals(IQ.Type.ERROR)) {
+ throw new XMPPException(iqResponse.getError());
+ }
+ else {
+ throw new XMPPException("File transfer response unreadable");
+ }
+ }
+ else {
+ return null;
+ }
+ }
+
+ private StreamNegotiator getOutgoingNegotiator(final FormField field)
+ throws XMPPException {
+ String variable;
+ boolean isByteStream = false;
+ boolean isIBB = false;
+ for (Iterator<String> it = field.getValues(); it.hasNext();) {
+ variable = it.next();
+ if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) {
+ isByteStream = true;
+ }
+ else if (variable.equals(InBandBytestreamManager.NAMESPACE)) {
+ isIBB = true;
+ }
+ }
+
+ if (!isByteStream && !isIBB) {
+ XMPPError error = new XMPPError(XMPPError.Condition.bad_request,
+ "No acceptable transfer mechanism");
+ throw new XMPPException(error.getMessage(), error);
+ }
+
+ if (isByteStream && isIBB) {
+ return new FaultTolerantNegotiator(connection,
+ byteStreamTransferManager, inbandTransferManager);
+ }
+ else if (isByteStream) {
+ return byteStreamTransferManager;
+ }
+ else {
+ return inbandTransferManager;
+ }
+ }
+
+ private DataForm createDefaultInitiationForm() {
+ DataForm form = new DataForm(Form.TYPE_FORM);
+ FormField field = new FormField(STREAM_DATA_FIELD_NAME);
+ field.setType(FormField.TYPE_LIST_SINGLE);
+ if (!IBB_ONLY) {
+ field.addOption(new FormField.Option(Socks5BytestreamManager.NAMESPACE));
+ }
+ field.addOption(new FormField.Option(InBandBytestreamManager.NAMESPACE));
+ form.addField(field);
+ return form;
+ }
+}
diff --git a/src/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java b/src/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java new file mode 100644 index 0000000..6b5ccd8 --- /dev/null +++ b/src/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java @@ -0,0 +1,138 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.filetransfer;
+
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+/**
+ * A request to send a file recieved from another user.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+public class FileTransferRequest {
+ private final StreamInitiation streamInitiation;
+
+ private final FileTransferManager manager;
+
+ /**
+ * A recieve request is constructed from the Stream Initiation request
+ * received from the initator.
+ *
+ * @param manager
+ * The manager handling this file transfer
+ *
+ * @param si
+ * The Stream initiaton recieved from the initiator.
+ */
+ public FileTransferRequest(FileTransferManager manager, StreamInitiation si) {
+ this.streamInitiation = si;
+ this.manager = manager;
+ }
+
+ /**
+ * Returns the name of the file.
+ *
+ * @return Returns the name of the file.
+ */
+ public String getFileName() {
+ return streamInitiation.getFile().getName();
+ }
+
+ /**
+ * Returns the size in bytes of the file.
+ *
+ * @return Returns the size in bytes of the file.
+ */
+ public long getFileSize() {
+ return streamInitiation.getFile().getSize();
+ }
+
+ /**
+ * Returns the description of the file provided by the requestor.
+ *
+ * @return Returns the description of the file provided by the requestor.
+ */
+ public String getDescription() {
+ return streamInitiation.getFile().getDesc();
+ }
+
+ /**
+ * Returns the mime-type of the file.
+ *
+ * @return Returns the mime-type of the file.
+ */
+ public String getMimeType() {
+ return streamInitiation.getMimeType();
+ }
+
+ /**
+ * Returns the fully-qualified jabber ID of the user that requested this
+ * file transfer.
+ *
+ * @return Returns the fully-qualified jabber ID of the user that requested
+ * this file transfer.
+ */
+ public String getRequestor() {
+ return streamInitiation.getFrom();
+ }
+
+ /**
+ * Returns the stream ID that uniquely identifies this file transfer.
+ *
+ * @return Returns the stream ID that uniquely identifies this file
+ * transfer.
+ */
+ public String getStreamID() {
+ return streamInitiation.getSessionID();
+ }
+
+ /**
+ * Returns the stream initiation packet that was sent by the requestor which
+ * contains the parameters of the file transfer being transfer and also the
+ * methods available to transfer the file.
+ *
+ * @return Returns the stream initiation packet that was sent by the
+ * requestor which contains the parameters of the file transfer
+ * being transfer and also the methods available to transfer the
+ * file.
+ */
+ protected StreamInitiation getStreamInitiation() {
+ return streamInitiation;
+ }
+
+ /**
+ * Accepts this file transfer and creates the incoming file transfer.
+ *
+ * @return Returns the <b><i>IncomingFileTransfer</b></i> on which the
+ * file transfer can be carried out.
+ */
+ public IncomingFileTransfer accept() {
+ return manager.createIncomingFileTransfer(this);
+ }
+
+ /**
+ * Rejects the file transfer request.
+ */
+ public void reject() {
+ manager.rejectIncomingFileTransfer(this);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java b/src/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java new file mode 100644 index 0000000..b32f49a --- /dev/null +++ b/src/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java @@ -0,0 +1,152 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.filetransfer;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.FromContainsFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamRequest;
+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamSession;
+import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+/**
+ * The In-Band Bytestream file transfer method, or IBB for short, transfers the
+ * file over the same XML Stream used by XMPP. It is the fall-back mechanism in
+ * case the SOCKS5 bytestream method of transferring files is not available.
+ *
+ * @author Alexander Wenckus
+ * @author Henning Staib
+ * @see <a href="http://xmpp.org/extensions/xep-0047.html">XEP-0047: In-Band
+ * Bytestreams (IBB)</a>
+ */
+public class IBBTransferNegotiator extends StreamNegotiator {
+
+ private Connection connection;
+
+ private InBandBytestreamManager manager;
+
+ /**
+ * The default constructor for the In-Band Bytestream Negotiator.
+ *
+ * @param connection The connection which this negotiator works on.
+ */
+ protected IBBTransferNegotiator(Connection connection) {
+ this.connection = connection;
+ this.manager = InBandBytestreamManager.getByteStreamManager(connection);
+ }
+
+ public OutputStream createOutgoingStream(String streamID, String initiator,
+ String target) throws XMPPException {
+ InBandBytestreamSession session = this.manager.establishSession(target, streamID);
+ session.setCloseBothStreamsEnabled(true);
+ return session.getOutputStream();
+ }
+
+ public InputStream createIncomingStream(StreamInitiation initiation)
+ throws XMPPException {
+ /*
+ * In-Band Bytestream initiation listener must ignore next in-band
+ * bytestream request with given session ID
+ */
+ this.manager.ignoreBytestreamRequestOnce(initiation.getSessionID());
+
+ Packet streamInitiation = initiateIncomingStream(this.connection, initiation);
+ return negotiateIncomingStream(streamInitiation);
+ }
+
+ public PacketFilter getInitiationPacketFilter(String from, String streamID) {
+ /*
+ * this method is always called prior to #negotiateIncomingStream() so
+ * the In-Band Bytestream initiation listener must ignore the next
+ * In-Band Bytestream request with the given session ID
+ */
+ this.manager.ignoreBytestreamRequestOnce(streamID);
+
+ return new AndFilter(new FromContainsFilter(from), new IBBOpenSidFilter(streamID));
+ }
+
+ public String[] getNamespaces() {
+ return new String[] { InBandBytestreamManager.NAMESPACE };
+ }
+
+ InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException {
+ // build In-Band Bytestream request
+ InBandBytestreamRequest request = new ByteStreamRequest(this.manager,
+ (Open) streamInitiation);
+
+ // always accept the request
+ InBandBytestreamSession session = request.accept();
+ session.setCloseBothStreamsEnabled(true);
+ return session.getInputStream();
+ }
+
+ public void cleanup() {
+ }
+
+ /**
+ * This PacketFilter accepts an incoming In-Band Bytestream open request
+ * with a specified session ID.
+ */
+ private static class IBBOpenSidFilter extends PacketTypeFilter {
+
+ private String sessionID;
+
+ public IBBOpenSidFilter(String sessionID) {
+ super(Open.class);
+ if (sessionID == null) {
+ throw new IllegalArgumentException("StreamID cannot be null");
+ }
+ this.sessionID = sessionID;
+ }
+
+ public boolean accept(Packet packet) {
+ if (super.accept(packet)) {
+ Open bytestream = (Open) packet;
+
+ // packet must by of type SET and contains the given session ID
+ return this.sessionID.equals(bytestream.getSessionID())
+ && IQ.Type.SET.equals(bytestream.getType());
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Derive from InBandBytestreamRequest to access protected constructor.
+ */
+ private static class ByteStreamRequest extends InBandBytestreamRequest {
+
+ private ByteStreamRequest(InBandBytestreamManager manager, Open byteStreamRequest) {
+ super(manager, byteStreamRequest);
+ }
+
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java b/src/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java new file mode 100644 index 0000000..91a5a0d --- /dev/null +++ b/src/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java @@ -0,0 +1,215 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.filetransfer;
+
+import org.jivesoftware.smack.XMPPException;
+
+import java.io.*;
+import java.util.concurrent.*;
+
+/**
+ * An incoming file transfer is created when the
+ * {@link FileTransferManager#createIncomingFileTransfer(FileTransferRequest)}
+ * method is invoked. It is a file being sent to the local user from another
+ * user on the jabber network. There are two stages of the file transfer to be
+ * concerned with and they can be handled in different ways depending upon the
+ * method that is invoked on this class.
+ * <p/>
+ * The first way that a file is recieved is by calling the
+ * {@link #recieveFile()} method. This method, negotiates the appropriate stream
+ * method and then returns the <b><i>InputStream</b></i> to read the file
+ * data from.
+ * <p/>
+ * The second way that a file can be recieved through this class is by invoking
+ * the {@link #recieveFile(File)} method. This method returns immediatly and
+ * takes as its parameter a file on the local file system where the file
+ * recieved from the transfer will be put.
+ *
+ * @author Alexander Wenckus
+ */
+public class IncomingFileTransfer extends FileTransfer {
+
+ private FileTransferRequest recieveRequest;
+
+ private InputStream inputStream;
+
+ protected IncomingFileTransfer(FileTransferRequest request,
+ FileTransferNegotiator transferNegotiator) {
+ super(request.getRequestor(), request.getStreamID(), transferNegotiator);
+ this.recieveRequest = request;
+ }
+
+ /**
+ * Negotiates the stream method to transfer the file over and then returns
+ * the negotiated stream.
+ *
+ * @return The negotiated InputStream from which to read the data.
+ * @throws XMPPException If there is an error in the negotiation process an exception
+ * is thrown.
+ */
+ public InputStream recieveFile() throws XMPPException {
+ if (inputStream != null) {
+ throw new IllegalStateException("Transfer already negotiated!");
+ }
+
+ try {
+ inputStream = negotiateStream();
+ }
+ catch (XMPPException e) {
+ setException(e);
+ throw e;
+ }
+
+ return inputStream;
+ }
+
+ /**
+ * This method negotitates the stream and then transfer's the file over the
+ * negotiated stream. The transfered file will be saved at the provided
+ * location.
+ * <p/>
+ * This method will return immedialtly, file transfer progress can be
+ * monitored through several methods:
+ * <p/>
+ * <UL>
+ * <LI>{@link FileTransfer#getStatus()}
+ * <LI>{@link FileTransfer#getProgress()}
+ * <LI>{@link FileTransfer#isDone()}
+ * </UL>
+ *
+ * @param file The location to save the file.
+ * @throws XMPPException when the file transfer fails
+ * @throws IllegalArgumentException This exception is thrown when the the provided file is
+ * either null, or cannot be written to.
+ */
+ public void recieveFile(final File file) throws XMPPException {
+ if (file != null) {
+ if (!file.exists()) {
+ try {
+ file.createNewFile();
+ }
+ catch (IOException e) {
+ throw new XMPPException(
+ "Could not create file to write too", e);
+ }
+ }
+ if (!file.canWrite()) {
+ throw new IllegalArgumentException("Cannot write to provided file");
+ }
+ }
+ else {
+ throw new IllegalArgumentException("File cannot be null");
+ }
+
+ Thread transferThread = new Thread(new Runnable() {
+ public void run() {
+ try {
+ inputStream = negotiateStream();
+ }
+ catch (XMPPException e) {
+ handleXMPPException(e);
+ return;
+ }
+
+ OutputStream outputStream = null;
+ try {
+ outputStream = new FileOutputStream(file);
+ setStatus(Status.in_progress);
+ writeToStream(inputStream, outputStream);
+ }
+ catch (XMPPException e) {
+ setStatus(Status.error);
+ setError(Error.stream);
+ setException(e);
+ }
+ catch (FileNotFoundException e) {
+ setStatus(Status.error);
+ setError(Error.bad_file);
+ setException(e);
+ }
+
+ if (getStatus().equals(Status.in_progress)) {
+ setStatus(Status.complete);
+ }
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ }
+ catch (Throwable io) {
+ /* Ignore */
+ }
+ }
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ }
+ catch (Throwable io) {
+ /* Ignore */
+ }
+ }
+ }
+ }, "File Transfer " + streamID);
+ transferThread.start();
+ }
+
+ private void handleXMPPException(XMPPException e) {
+ setStatus(FileTransfer.Status.error);
+ setException(e);
+ }
+
+ private InputStream negotiateStream() throws XMPPException {
+ setStatus(Status.negotiating_transfer);
+ final StreamNegotiator streamNegotiator = negotiator
+ .selectStreamNegotiator(recieveRequest);
+ setStatus(Status.negotiating_stream);
+ FutureTask<InputStream> streamNegotiatorTask = new FutureTask<InputStream>(
+ new Callable<InputStream>() {
+
+ public InputStream call() throws Exception {
+ return streamNegotiator
+ .createIncomingStream(recieveRequest.getStreamInitiation());
+ }
+ });
+ streamNegotiatorTask.run();
+ InputStream inputStream;
+ try {
+ inputStream = streamNegotiatorTask.get(15, TimeUnit.SECONDS);
+ }
+ catch (InterruptedException e) {
+ throw new XMPPException("Interruption while executing", e);
+ }
+ catch (ExecutionException e) {
+ throw new XMPPException("Error in execution", e);
+ }
+ catch (TimeoutException e) {
+ throw new XMPPException("Request timed out", e);
+ }
+ finally {
+ streamNegotiatorTask.cancel(true);
+ }
+ setStatus(Status.negotiated);
+ return inputStream;
+ }
+
+ public void cancel() {
+ setStatus(Status.cancelled);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java b/src/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java new file mode 100644 index 0000000..bba6c38 --- /dev/null +++ b/src/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java @@ -0,0 +1,456 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.filetransfer;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.XMPPError;
+
+import java.io.*;
+
+/**
+ * Handles the sending of a file to another user. File transfer's in jabber have
+ * several steps and there are several methods in this class that handle these
+ * steps differently.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+public class OutgoingFileTransfer extends FileTransfer {
+
+ private static int RESPONSE_TIMEOUT = 60 * 1000;
+ private NegotiationProgress callback;
+
+ /**
+ * Returns the time in milliseconds after which the file transfer
+ * negotiation process will timeout if the other user has not responded.
+ *
+ * @return Returns the time in milliseconds after which the file transfer
+ * negotiation process will timeout if the remote user has not
+ * responded.
+ */
+ public static int getResponseTimeout() {
+ return RESPONSE_TIMEOUT;
+ }
+
+ /**
+ * Sets the time in milliseconds after which the file transfer negotiation
+ * process will timeout if the other user has not responded.
+ *
+ * @param responseTimeout
+ * The timeout time in milliseconds.
+ */
+ public static void setResponseTimeout(int responseTimeout) {
+ RESPONSE_TIMEOUT = responseTimeout;
+ }
+
+ private OutputStream outputStream;
+
+ private String initiator;
+
+ private Thread transferThread;
+
+ protected OutgoingFileTransfer(String initiator, String target,
+ String streamID, FileTransferNegotiator transferNegotiator) {
+ super(target, streamID, transferNegotiator);
+ this.initiator = initiator;
+ }
+
+ protected void setOutputStream(OutputStream stream) {
+ if (outputStream == null) {
+ this.outputStream = stream;
+ }
+ }
+
+ /**
+ * Returns the output stream connected to the peer to transfer the file. It
+ * is only available after it has been successfully negotiated by the
+ * {@link StreamNegotiator}.
+ *
+ * @return Returns the output stream connected to the peer to transfer the
+ * file.
+ */
+ protected OutputStream getOutputStream() {
+ if (getStatus().equals(FileTransfer.Status.negotiated)) {
+ return outputStream;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * This method handles the negotiation of the file transfer and the stream,
+ * it only returns the created stream after the negotiation has been completed.
+ *
+ * @param fileName
+ * The name of the file that will be transmitted. It is
+ * preferable for this name to have an extension as it will be
+ * used to determine the type of file it is.
+ * @param fileSize
+ * The size in bytes of the file that will be transmitted.
+ * @param description
+ * A description of the file that will be transmitted.
+ * @return The OutputStream that is connected to the peer to transmit the
+ * file.
+ * @throws XMPPException
+ * Thrown if an error occurs during the file transfer
+ * negotiation process.
+ */
+ public synchronized OutputStream sendFile(String fileName, long fileSize,
+ String description) throws XMPPException {
+ if (isDone() || outputStream != null) {
+ throw new IllegalStateException(
+ "The negotation process has already"
+ + " been attempted on this file transfer");
+ }
+ try {
+ setFileInfo(fileName, fileSize);
+ this.outputStream = negotiateStream(fileName, fileSize, description);
+ } catch (XMPPException e) {
+ handleXMPPException(e);
+ throw e;
+ }
+ return outputStream;
+ }
+
+ /**
+ * This methods handles the transfer and stream negotiation process. It
+ * returns immediately and its progress will be updated through the
+ * {@link NegotiationProgress} callback.
+ *
+ * @param fileName
+ * The name of the file that will be transmitted. It is
+ * preferable for this name to have an extension as it will be
+ * used to determine the type of file it is.
+ * @param fileSize
+ * The size in bytes of the file that will be transmitted.
+ * @param description
+ * A description of the file that will be transmitted.
+ * @param progress
+ * A callback to monitor the progress of the file transfer
+ * negotiation process and to retrieve the OutputStream when it
+ * is complete.
+ */
+ public synchronized void sendFile(final String fileName,
+ final long fileSize, final String description,
+ final NegotiationProgress progress)
+ {
+ if(progress == null) {
+ throw new IllegalArgumentException("Callback progress cannot be null.");
+ }
+ checkTransferThread();
+ if (isDone() || outputStream != null) {
+ throw new IllegalStateException(
+ "The negotation process has already"
+ + " been attempted for this file transfer");
+ }
+ setFileInfo(fileName, fileSize);
+ this.callback = progress;
+ transferThread = new Thread(new Runnable() {
+ public void run() {
+ try {
+ OutgoingFileTransfer.this.outputStream = negotiateStream(
+ fileName, fileSize, description);
+ progress.outputStreamEstablished(OutgoingFileTransfer.this.outputStream);
+ }
+ catch (XMPPException e) {
+ handleXMPPException(e);
+ }
+ }
+ }, "File Transfer Negotiation " + streamID);
+ transferThread.start();
+ }
+
+ private void checkTransferThread() {
+ if (transferThread != null && transferThread.isAlive() || isDone()) {
+ throw new IllegalStateException(
+ "File transfer in progress or has already completed.");
+ }
+ }
+
+ /**
+ * This method handles the stream negotiation process and transmits the file
+ * to the remote user. It returns immediately and the progress of the file
+ * transfer can be monitored through several methods:
+ *
+ * <UL>
+ * <LI>{@link FileTransfer#getStatus()}
+ * <LI>{@link FileTransfer#getProgress()}
+ * <LI>{@link FileTransfer#isDone()}
+ * </UL>
+ *
+ * @param file the file to transfer to the remote entity.
+ * @param description a description for the file to transfer.
+ * @throws XMPPException
+ * If there is an error during the negotiation process or the
+ * sending of the file.
+ */
+ public synchronized void sendFile(final File file, final String description)
+ throws XMPPException {
+ checkTransferThread();
+ if (file == null || !file.exists() || !file.canRead()) {
+ throw new IllegalArgumentException("Could not read file");
+ } else {
+ setFileInfo(file.getAbsolutePath(), file.getName(), file.length());
+ }
+
+ transferThread = new Thread(new Runnable() {
+ public void run() {
+ try {
+ outputStream = negotiateStream(file.getName(), file
+ .length(), description);
+ } catch (XMPPException e) {
+ handleXMPPException(e);
+ return;
+ }
+ if (outputStream == null) {
+ return;
+ }
+
+ if (!updateStatus(Status.negotiated, Status.in_progress)) {
+ return;
+ }
+
+ InputStream inputStream = null;
+ try {
+ inputStream = new FileInputStream(file);
+ writeToStream(inputStream, outputStream);
+ } catch (FileNotFoundException e) {
+ setStatus(FileTransfer.Status.error);
+ setError(Error.bad_file);
+ setException(e);
+ } catch (XMPPException e) {
+ setStatus(FileTransfer.Status.error);
+ setException(e);
+ } finally {
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+
+ outputStream.flush();
+ outputStream.close();
+ } catch (IOException e) {
+ /* Do Nothing */
+ }
+ }
+ updateStatus(Status.in_progress, FileTransfer.Status.complete);
+ }
+
+ }, "File Transfer " + streamID);
+ transferThread.start();
+ }
+
+ /**
+ * This method handles the stream negotiation process and transmits the file
+ * to the remote user. It returns immediately and the progress of the file
+ * transfer can be monitored through several methods:
+ *
+ * <UL>
+ * <LI>{@link FileTransfer#getStatus()}
+ * <LI>{@link FileTransfer#getProgress()}
+ * <LI>{@link FileTransfer#isDone()}
+ * </UL>
+ *
+ * @param in the stream to transfer to the remote entity.
+ * @param fileName the name of the file that is transferred
+ * @param fileSize the size of the file that is transferred
+ * @param description a description for the file to transfer.
+ */
+ public synchronized void sendStream(final InputStream in, final String fileName, final long fileSize, final String description){
+ checkTransferThread();
+
+ setFileInfo(fileName, fileSize);
+ transferThread = new Thread(new Runnable() {
+ public void run() {
+ setFileInfo(fileName, fileSize);
+ //Create packet filter
+ try {
+ outputStream = negotiateStream(fileName, fileSize, description);
+ } catch (XMPPException e) {
+ handleXMPPException(e);
+ return;
+ } catch (IllegalStateException e) {
+ setStatus(FileTransfer.Status.error);
+ setException(e);
+ }
+ if (outputStream == null) {
+ return;
+ }
+
+ if (!updateStatus(Status.negotiated, Status.in_progress)) {
+ return;
+ }
+ try {
+ writeToStream(in, outputStream);
+ } catch (XMPPException e) {
+ setStatus(FileTransfer.Status.error);
+ setException(e);
+ } catch (IllegalStateException e) {
+ setStatus(FileTransfer.Status.error);
+ setException(e);
+ } finally {
+ try {
+ if (in != null) {
+ in.close();
+ }
+
+ outputStream.flush();
+ outputStream.close();
+ } catch (IOException e) {
+ /* Do Nothing */
+ }
+ }
+ updateStatus(Status.in_progress, FileTransfer.Status.complete);
+ }
+
+ }, "File Transfer " + streamID);
+ transferThread.start();
+ }
+
+ private void handleXMPPException(XMPPException e) {
+ XMPPError error = e.getXMPPError();
+ if (error != null) {
+ int code = error.getCode();
+ if (code == 403) {
+ setStatus(Status.refused);
+ return;
+ }
+ else if (code == 400) {
+ setStatus(Status.error);
+ setError(Error.not_acceptable);
+ }
+ else {
+ setStatus(FileTransfer.Status.error);
+ }
+ }
+
+ setException(e);
+ }
+
+ /**
+ * Returns the amount of bytes that have been sent for the file transfer. Or
+ * -1 if the file transfer has not started.
+ * <p>
+ * Note: This method is only useful when the {@link #sendFile(File, String)}
+ * method is called, as it is the only method that actually transmits the
+ * file.
+ *
+ * @return Returns the amount of bytes that have been sent for the file
+ * transfer. Or -1 if the file transfer has not started.
+ */
+ public long getBytesSent() {
+ return amountWritten;
+ }
+
+ private OutputStream negotiateStream(String fileName, long fileSize,
+ String description) throws XMPPException {
+ // Negotiate the file transfer profile
+
+ if (!updateStatus(Status.initial, Status.negotiating_transfer)) {
+ throw new XMPPException("Illegal state change");
+ }
+ StreamNegotiator streamNegotiator = negotiator.negotiateOutgoingTransfer(
+ getPeer(), streamID, fileName, fileSize, description,
+ RESPONSE_TIMEOUT);
+
+ if (streamNegotiator == null) {
+ setStatus(Status.error);
+ setError(Error.no_response);
+ return null;
+ }
+
+ // Negotiate the stream
+ if (!updateStatus(Status.negotiating_transfer, Status.negotiating_stream)) {
+ throw new XMPPException("Illegal state change");
+ }
+ outputStream = streamNegotiator.createOutgoingStream(streamID,
+ initiator, getPeer());
+
+ if (!updateStatus(Status.negotiating_stream, Status.negotiated)) {
+ throw new XMPPException("Illegal state change");
+ }
+ return outputStream;
+ }
+
+ public void cancel() {
+ setStatus(Status.cancelled);
+ }
+
+ @Override
+ protected boolean updateStatus(Status oldStatus, Status newStatus) {
+ boolean isUpdated = super.updateStatus(oldStatus, newStatus);
+ if(callback != null && isUpdated) {
+ callback.statusUpdated(oldStatus, newStatus);
+ }
+ return isUpdated;
+ }
+
+ @Override
+ protected void setStatus(Status status) {
+ Status oldStatus = getStatus();
+ super.setStatus(status);
+ if(callback != null) {
+ callback.statusUpdated(oldStatus, status);
+ }
+ }
+
+ @Override
+ protected void setException(Exception exception) {
+ super.setException(exception);
+ if(callback != null) {
+ callback.errorEstablishingStream(exception);
+ }
+ }
+
+ /**
+ * A callback class to retrieve the status of an outgoing transfer
+ * negotiation process.
+ *
+ * @author Alexander Wenckus
+ *
+ */
+ public interface NegotiationProgress {
+
+ /**
+ * Called when the status changes
+ *
+ * @param oldStatus the previous status of the file transfer.
+ * @param newStatus the new status of the file transfer.
+ */
+ void statusUpdated(Status oldStatus, Status newStatus);
+
+ /**
+ * Once the negotiation process is completed the output stream can be
+ * retrieved.
+ *
+ * @param stream the established stream which can be used to transfer the file to the remote
+ * entity
+ */
+ void outputStreamEstablished(OutputStream stream);
+
+ /**
+ * Called when an exception occurs during the negotiation progress.
+ *
+ * @param e the exception that occurred.
+ */
+ void errorEstablishingStream(Exception e);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java b/src/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java new file mode 100644 index 0000000..3c07fdc --- /dev/null +++ b/src/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java @@ -0,0 +1,164 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.filetransfer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PushbackInputStream;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.FromMatchesFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;
+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamRequest;
+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamSession;
+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+/**
+ * Negotiates a SOCKS5 Bytestream to be used for file transfers. The implementation is based on the
+ * {@link Socks5BytestreamManager} and the {@link Socks5BytestreamRequest}.
+ *
+ * @author Henning Staib
+ * @see <a href="http://xmpp.org/extensions/xep-0065.html">XEP-0065: SOCKS5 Bytestreams</a>
+ */
+public class Socks5TransferNegotiator extends StreamNegotiator {
+
+ private Connection connection;
+
+ private Socks5BytestreamManager manager;
+
+ Socks5TransferNegotiator(Connection connection) {
+ this.connection = connection;
+ this.manager = Socks5BytestreamManager.getBytestreamManager(this.connection);
+ }
+
+ @Override
+ public OutputStream createOutgoingStream(String streamID, String initiator, String target)
+ throws XMPPException {
+ try {
+ return this.manager.establishSession(target, streamID).getOutputStream();
+ }
+ catch (IOException e) {
+ throw new XMPPException("error establishing SOCKS5 Bytestream", e);
+ }
+ catch (InterruptedException e) {
+ throw new XMPPException("error establishing SOCKS5 Bytestream", e);
+ }
+ }
+
+ @Override
+ public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException,
+ InterruptedException {
+ /*
+ * SOCKS5 initiation listener must ignore next SOCKS5 Bytestream request with given session
+ * ID
+ */
+ this.manager.ignoreBytestreamRequestOnce(initiation.getSessionID());
+
+ Packet streamInitiation = initiateIncomingStream(this.connection, initiation);
+ return negotiateIncomingStream(streamInitiation);
+ }
+
+ @Override
+ public PacketFilter getInitiationPacketFilter(final String from, String streamID) {
+ /*
+ * this method is always called prior to #negotiateIncomingStream() so the SOCKS5
+ * InitiationListener must ignore the next SOCKS5 Bytestream request with the given session
+ * ID
+ */
+ this.manager.ignoreBytestreamRequestOnce(streamID);
+
+ return new AndFilter(new FromMatchesFilter(from), new BytestreamSIDFilter(streamID));
+ }
+
+ @Override
+ public String[] getNamespaces() {
+ return new String[] { Socks5BytestreamManager.NAMESPACE };
+ }
+
+ @Override
+ InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException,
+ InterruptedException {
+ // build SOCKS5 Bytestream request
+ Socks5BytestreamRequest request = new ByteStreamRequest(this.manager,
+ (Bytestream) streamInitiation);
+
+ // always accept the request
+ Socks5BytestreamSession session = request.accept();
+
+ // test input stream
+ try {
+ PushbackInputStream stream = new PushbackInputStream(session.getInputStream());
+ int firstByte = stream.read();
+ stream.unread(firstByte);
+ return stream;
+ }
+ catch (IOException e) {
+ throw new XMPPException("Error establishing input stream", e);
+ }
+ }
+
+ @Override
+ public void cleanup() {
+ /* do nothing */
+ }
+
+ /**
+ * This PacketFilter accepts an incoming SOCKS5 Bytestream request with a specified session ID.
+ */
+ private static class BytestreamSIDFilter extends PacketTypeFilter {
+
+ private String sessionID;
+
+ public BytestreamSIDFilter(String sessionID) {
+ super(Bytestream.class);
+ if (sessionID == null) {
+ throw new IllegalArgumentException("StreamID cannot be null");
+ }
+ this.sessionID = sessionID;
+ }
+
+ @Override
+ public boolean accept(Packet packet) {
+ if (super.accept(packet)) {
+ Bytestream bytestream = (Bytestream) packet;
+
+ // packet must by of type SET and contains the given session ID
+ return this.sessionID.equals(bytestream.getSessionID())
+ && IQ.Type.SET.equals(bytestream.getType());
+ }
+ return false;
+ }
+
+ }
+
+ /**
+ * Derive from Socks5BytestreamRequest to access protected constructor.
+ */
+ private static class ByteStreamRequest extends Socks5BytestreamRequest {
+
+ private ByteStreamRequest(Socks5BytestreamManager manager, Bytestream byteStreamRequest) {
+ super(manager, byteStreamRequest);
+ }
+
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java b/src/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java new file mode 100644 index 0000000..5eefe43 --- /dev/null +++ b/src/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java @@ -0,0 +1,167 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.filetransfer;
+
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * After the file transfer negotiation process is completed according to
+ * JEP-0096, the negotiation process is passed off to a particular stream
+ * negotiator. The stream negotiator will then negotiate the chosen stream and
+ * return the stream to transfer the file.
+ *
+ * @author Alexander Wenckus
+ */
+public abstract class StreamNegotiator {
+
+ /**
+ * Creates the initiation acceptance packet to forward to the stream
+ * initiator.
+ *
+ * @param streamInitiationOffer The offer from the stream initiator to connect for a stream.
+ * @param namespaces The namespace that relates to the accepted means of transfer.
+ * @return The response to be forwarded to the initiator.
+ */
+ public StreamInitiation createInitiationAccept(
+ StreamInitiation streamInitiationOffer, String[] namespaces)
+ {
+ StreamInitiation response = new StreamInitiation();
+ response.setTo(streamInitiationOffer.getFrom());
+ response.setFrom(streamInitiationOffer.getTo());
+ response.setType(IQ.Type.RESULT);
+ response.setPacketID(streamInitiationOffer.getPacketID());
+
+ DataForm form = new DataForm(Form.TYPE_SUBMIT);
+ FormField field = new FormField(
+ FileTransferNegotiator.STREAM_DATA_FIELD_NAME);
+ for (String namespace : namespaces) {
+ field.addValue(namespace);
+ }
+ form.addField(field);
+
+ response.setFeatureNegotiationForm(form);
+ return response;
+ }
+
+
+ public IQ createError(String from, String to, String packetID, XMPPError xmppError) {
+ IQ iq = FileTransferNegotiator.createIQ(packetID, to, from, IQ.Type.ERROR);
+ iq.setError(xmppError);
+ return iq;
+ }
+
+ Packet initiateIncomingStream(Connection connection, StreamInitiation initiation) throws XMPPException {
+ StreamInitiation response = createInitiationAccept(initiation,
+ getNamespaces());
+
+ // establish collector to await response
+ PacketCollector collector = connection
+ .createPacketCollector(getInitiationPacketFilter(initiation.getFrom(), initiation.getSessionID()));
+ connection.sendPacket(response);
+
+ Packet streamMethodInitiation = collector
+ .nextResult(SmackConfiguration.getPacketReplyTimeout());
+ collector.cancel();
+ if (streamMethodInitiation == null) {
+ throw new XMPPException("No response from file transfer initiator");
+ }
+
+ return streamMethodInitiation;
+ }
+
+ /**
+ * Returns the packet filter that will return the initiation packet for the appropriate stream
+ * initiation.
+ *
+ * @param from The initiator of the file transfer.
+ * @param streamID The stream ID related to the transfer.
+ * @return The <b><i>PacketFilter</b></i> that will return the packet relatable to the stream
+ * initiation.
+ */
+ public abstract PacketFilter getInitiationPacketFilter(String from, String streamID);
+
+
+ abstract InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException,
+ InterruptedException;
+
+ /**
+ * This method handles the file stream download negotiation process. The
+ * appropriate stream negotiator's initiate incoming stream is called after
+ * an appropriate file transfer method is selected. The manager will respond
+ * to the initiator with the selected means of transfer, then it will handle
+ * any negotiation specific to the particular transfer method. This method
+ * returns the InputStream, ready to transfer the file.
+ *
+ * @param initiation The initiation that triggered this download.
+ * @return After the negotiation process is complete, the InputStream to
+ * write a file to is returned.
+ * @throws XMPPException If an error occurs during this process an XMPPException is
+ * thrown.
+ * @throws InterruptedException If thread is interrupted.
+ */
+ public abstract InputStream createIncomingStream(StreamInitiation initiation)
+ throws XMPPException, InterruptedException;
+
+ /**
+ * This method handles the file upload stream negotiation process. The
+ * particular stream negotiator is determined during the file transfer
+ * negotiation process. This method returns the OutputStream to transmit the
+ * file to the remote user.
+ *
+ * @param streamID The streamID that uniquely identifies the file transfer.
+ * @param initiator The fully-qualified JID of the initiator of the file transfer.
+ * @param target The fully-qualified JID of the target or receiver of the file
+ * transfer.
+ * @return The negotiated stream ready for data.
+ * @throws XMPPException If an error occurs during the negotiation process an
+ * exception will be thrown.
+ */
+ public abstract OutputStream createOutgoingStream(String streamID,
+ String initiator, String target) throws XMPPException;
+
+ /**
+ * Returns the XMPP namespace reserved for this particular type of file
+ * transfer.
+ *
+ * @return Returns the XMPP namespace reserved for this particular type of
+ * file transfer.
+ */
+ public abstract String[] getNamespaces();
+
+ /**
+ * Cleanup any and all resources associated with this negotiator.
+ */
+ public abstract void cleanup();
+
+}
diff --git a/src/org/jivesoftware/smackx/forward/Forwarded.java b/src/org/jivesoftware/smackx/forward/Forwarded.java new file mode 100644 index 0000000..817ee27 --- /dev/null +++ b/src/org/jivesoftware/smackx/forward/Forwarded.java @@ -0,0 +1,125 @@ +/** + * Copyright 2013 Georg Lukas + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.forward; + +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.packet.DelayInfo; +import org.jivesoftware.smackx.provider.DelayInfoProvider; +import org.xmlpull.v1.XmlPullParser; + +/** + * Packet extension for XEP-0297: Stanza Forwarding. This class implements + * the packet extension and a {@link PacketExtensionProvider} to parse + * forwarded messages from a packet. The extension + * <a href="http://xmpp.org/extensions/xep-0297.html">XEP-0297</a> is + * a prerequisite for XEP-0280 (Message Carbons). + * + * <p>The {@link Forwarded.Provider} must be registered in the + * <b>smack.properties</b> file for the element <b>forwarded</b> with + * namespace <b>urn:xmpp:forwarded:0</b></p> to be used. + * + * @author Georg Lukas + */ +public class Forwarded implements PacketExtension { + public static final String NAMESPACE = "urn:xmpp:forward:0"; + public static final String ELEMENT_NAME = "forwarded"; + + private DelayInfo delay; + private Packet forwardedPacket; + + /** + * Creates a new Forwarded packet extension. + * + * @param delay an optional {@link DelayInfo} timestamp of the packet. + * @param fwdPacket the packet that is forwarded (required). + */ + public Forwarded(DelayInfo delay, Packet fwdPacket) { + this.delay = delay; + this.forwardedPacket = fwdPacket; + } + + @Override + public String getElementName() { + return ELEMENT_NAME; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"") + .append(getNamespace()).append("\">"); + + if (delay != null) + buf.append(delay.toXML()); + buf.append(forwardedPacket.toXML()); + + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + /** + * get the packet forwarded by this stanza. + * + * @return the {@link Packet} instance (typically a message) that was forwarded. + */ + public Packet getForwardedPacket() { + return forwardedPacket; + } + + /** + * get the timestamp of the forwarded packet. + * + * @return the {@link DelayInfo} representing the time when the original packet was sent. May be null. + */ + public DelayInfo getDelayInfo() { + return delay; + } + + public static class Provider implements PacketExtensionProvider { + DelayInfoProvider dip = new DelayInfoProvider(); + + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + DelayInfo di = null; + Packet packet = null; + + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("delay")) + di = (DelayInfo)dip.parseExtension(parser); + else if (parser.getName().equals("message")) + packet = PacketParserUtils.parseMessage(parser); + else throw new Exception("Unsupported forwarded packet type: " + parser.getName()); + } + else if (eventType == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME)) + done = true; + } + if (packet == null) + throw new Exception("forwarded extension must contain a packet"); + return new Forwarded(di, packet); + } + } +} diff --git a/src/org/jivesoftware/smackx/muc/Affiliate.java b/src/org/jivesoftware/smackx/muc/Affiliate.java new file mode 100644 index 0000000..09a04f6 --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/Affiliate.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.smackx.muc; + +import org.jivesoftware.smackx.packet.MUCAdmin; +import org.jivesoftware.smackx.packet.MUCOwner; + +/** + * Represents an affiliation of a user to a given room. The affiliate's information will always have + * the bare jid of the real user and its affiliation. If the affiliate is an occupant of the room + * then we will also have information about the role and nickname of the user in the room. + * + * @author Gaston Dombiak + */ +public class Affiliate { + // Fields that must have a value + private String jid; + private String affiliation; + + // Fields that may have a value + private String role; + private String nick; + + Affiliate(MUCOwner.Item item) { + super(); + this.jid = item.getJid(); + this.affiliation = item.getAffiliation(); + this.role = item.getRole(); + this.nick = item.getNick(); + } + + Affiliate(MUCAdmin.Item item) { + super(); + this.jid = item.getJid(); + this.affiliation = item.getAffiliation(); + this.role = item.getRole(); + this.nick = item.getNick(); + } + + /** + * Returns the bare JID of the affiliated user. This information will always be available. + * + * @return the bare JID of the affiliated user. + */ + public String getJid() { + return jid; + } + + /** + * Returns the affiliation of the afffiliated user. Possible affiliations are: "owner", "admin", + * "member", "outcast". This information will always be available. + * + * @return the affiliation of the afffiliated user. + */ + public String getAffiliation() { + return affiliation; + } + + /** + * Returns the current role of the affiliated user if the user is currently in the room. + * If the user is not present in the room then the answer will be null. + * + * @return the current role of the affiliated user in the room or null if the user is not in + * the room. + */ + public String getRole() { + return role; + } + + /** + * Returns the current nickname of the affiliated user if the user is currently in the room. + * If the user is not present in the room then the answer will be null. + * + * @return the current nickname of the affiliated user in the room or null if the user is not in + * the room. + */ + public String getNick() { + return nick; + } +} diff --git a/src/org/jivesoftware/smackx/muc/ConnectionDetachedPacketCollector.java b/src/org/jivesoftware/smackx/muc/ConnectionDetachedPacketCollector.java new file mode 100644 index 0000000..243c298 --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/ConnectionDetachedPacketCollector.java @@ -0,0 +1,123 @@ +/** + * $RCSfile$ + * $Revision: 2779 $ + * $Date: 2005-09-05 17:00:45 -0300 (Mon, 05 Sep 2005) $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.muc; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.packet.Packet; + +/** + * A variant of the {@link org.jivesoftware.smack.PacketCollector} class + * that does not force attachment to a <code>Connection</code> + * on creation and no filter is required. Used to collect message + * packets targeted to a group chat room. + * + * @author Larry Kirschner + */ +class ConnectionDetachedPacketCollector { + /** + * Max number of packets that any one collector can hold. After the max is + * reached, older packets will be automatically dropped from the queue as + * new packets are added. + */ + private int maxPackets = SmackConfiguration.getPacketCollectorSize(); + + private ArrayBlockingQueue<Packet> resultQueue; + + /** + * Creates a new packet collector. If the packet filter is <tt>null</tt>, then + * all packets will match this collector. + */ + public ConnectionDetachedPacketCollector() { + this(SmackConfiguration.getPacketCollectorSize()); + } + + /** + * Creates a new packet collector. If the packet filter is <tt>null</tt>, then + * all packets will match this collector. + */ + public ConnectionDetachedPacketCollector(int maxSize) { + this.resultQueue = new ArrayBlockingQueue<Packet>(maxSize); + } + + /** + * 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; + } + + 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/smackx/muc/DeafOccupantInterceptor.java b/src/org/jivesoftware/smackx/muc/DeafOccupantInterceptor.java new file mode 100644 index 0000000..24570fd --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/DeafOccupantInterceptor.java @@ -0,0 +1,76 @@ +/** + * $RCSfile$ + * $Revision: $ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.muc; + +import org.jivesoftware.smack.PacketInterceptor; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.packet.Presence; + +/** + * Packet interceptor that will intercept presence packets sent to the MUC service to indicate + * that the user wants to be a deaf occupant. A user can only indicate that he wants to be a + * deaf occupant while joining the room. It is not possible to become deaf or stop being deaf + * after the user joined the room.<p> + * + * Deaf occupants will not get messages broadcasted to all room occupants. However, they will + * be able to get private messages, presences, IQ packets or room history. To use this + * functionality you will need to send the message + * {@link MultiUserChat#addPresenceInterceptor(org.jivesoftware.smack.PacketInterceptor)} and + * pass this interceptor as the parameter.<p> + * + * Note that this is a custom extension to the MUC service so it may not work with other servers + * than Wildfire. + * + * @author Gaston Dombiak + */ +public class DeafOccupantInterceptor implements PacketInterceptor { + + public void interceptPacket(Packet packet) { + Presence presence = (Presence) packet; + // Check if user is joining a room + if (Presence.Type.available == presence.getType() && + presence.getExtension("x", "http://jabber.org/protocol/muc") != null) { + // Add extension that indicates that user wants to be a deaf occupant + packet.addExtension(new DeafExtension()); + } + } + + private static class DeafExtension implements PacketExtension { + + public String getElementName() { + return "x"; + } + + public String getNamespace() { + return "http://jivesoftware.org/protocol/muc"; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()) + .append("\">"); + buf.append("<deaf-occupant/>"); + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + } +} diff --git a/src/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java b/src/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java new file mode 100644 index 0000000..6eb9efa --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.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.smackx.muc; + +/** + * Default implementation of the ParticipantStatusListener interface.<p> + * + * This class does not provide any behavior by default. It just avoids having + * to implement all the inteface methods if the user is only interested in implementing + * some of the methods. + * + * @author Gaston Dombiak + */ +public class DefaultParticipantStatusListener implements ParticipantStatusListener { + + public void joined(String participant) { + } + + public void left(String participant) { + } + + public void kicked(String participant, String actor, String reason) { + } + + public void voiceGranted(String participant) { + } + + public void voiceRevoked(String participant) { + } + + public void banned(String participant, String actor, String reason) { + } + + public void membershipGranted(String participant) { + } + + public void membershipRevoked(String participant) { + } + + public void moderatorGranted(String participant) { + } + + public void moderatorRevoked(String participant) { + } + + public void ownershipGranted(String participant) { + } + + public void ownershipRevoked(String participant) { + } + + public void adminGranted(String participant) { + } + + public void adminRevoked(String participant) { + } + + public void nicknameChanged(String participant, String newNickname) { + } + +} diff --git a/src/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java b/src/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java new file mode 100644 index 0000000..de7cc87 --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java @@ -0,0 +1,70 @@ +/** + * $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.smackx.muc; + +/** + * Default implementation of the UserStatusListener interface.<p> + * + * This class does not provide any behavior by default. It just avoids having + * to implement all the inteface methods if the user is only interested in implementing + * some of the methods. + * + * @author Gaston Dombiak + */ +public class DefaultUserStatusListener implements UserStatusListener { + + public void kicked(String actor, String reason) { + } + + public void voiceGranted() { + } + + public void voiceRevoked() { + } + + public void banned(String actor, String reason) { + } + + public void membershipGranted() { + } + + public void membershipRevoked() { + } + + public void moderatorGranted() { + } + + public void moderatorRevoked() { + } + + public void ownershipGranted() { + } + + public void ownershipRevoked() { + } + + public void adminGranted() { + } + + public void adminRevoked() { + } + +} diff --git a/src/org/jivesoftware/smackx/muc/DiscussionHistory.java b/src/org/jivesoftware/smackx/muc/DiscussionHistory.java new file mode 100644 index 0000000..036f6cb --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/DiscussionHistory.java @@ -0,0 +1,173 @@ +/** + * $RCSfile$ +/** + * $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.smackx.muc; + +import java.util.Date; + +import org.jivesoftware.smackx.packet.MUCInitialPresence; + +/** + * The DiscussionHistory class controls the number of characters or messages to receive + * when entering a room. The room will decide the amount of history to return if you don't + * specify a DiscussionHistory while joining a room.<p> + * + * You can use some or all of these variable to control the amount of history to receive: + * <ul> + * <li>maxchars -> total number of characters to receive in the history. + * <li>maxstanzas -> total number of messages to receive in the history. + * <li>seconds -> only the messages received in the last "X" seconds will be included in the + * history. + * <li>since -> only the messages received since the datetime specified will be included in + * the history. + * </ul> + * + * Note: Setting maxchars to 0 indicates that the user requests to receive no history. + * + * @author Gaston Dombiak + */ +public class DiscussionHistory { + + private int maxChars = -1; + private int maxStanzas = -1; + private int seconds = -1; + private Date since; + + /** + * Returns the total number of characters to receive in the history. + * + * @return total number of characters to receive in the history. + */ + public int getMaxChars() { + return maxChars; + } + + /** + * Returns the total number of messages to receive in the history. + * + * @return the total number of messages to receive in the history. + */ + public int getMaxStanzas() { + return maxStanzas; + } + + /** + * Returns the number of seconds to use to filter the messages received during that time. + * In other words, only the messages received in the last "X" seconds will be included in + * the history. + * + * @return the number of seconds to use to filter the messages received during that time. + */ + public int getSeconds() { + return seconds; + } + + /** + * Returns the since date to use to filter the messages received during that time. + * In other words, only the messages received since the datetime specified will be + * included in the history. + * + * @return the since date to use to filter the messages received during that time. + */ + public Date getSince() { + return since; + } + + /** + * Sets the total number of characters to receive in the history. + * + * @param maxChars the total number of characters to receive in the history. + */ + public void setMaxChars(int maxChars) { + this.maxChars = maxChars; + } + + /** + * Sets the total number of messages to receive in the history. + * + * @param maxStanzas the total number of messages to receive in the history. + */ + public void setMaxStanzas(int maxStanzas) { + this.maxStanzas = maxStanzas; + } + + /** + * Sets the number of seconds to use to filter the messages received during that time. + * In other words, only the messages received in the last "X" seconds will be included in + * the history. + * + * @param seconds the number of seconds to use to filter the messages received during + * that time. + */ + public void setSeconds(int seconds) { + this.seconds = seconds; + } + + /** + * Sets the since date to use to filter the messages received during that time. + * In other words, only the messages received since the datetime specified will be + * included in the history. + * + * @param since the since date to use to filter the messages received during that time. + */ + public void setSince(Date since) { + this.since = since; + } + + /** + * Returns true if the history has been configured with some values. + * + * @return true if the history has been configured with some values. + */ + private boolean isConfigured() { + return maxChars > -1 || maxStanzas > -1 || seconds > -1 || since != null; + } + + /** + * Returns the History that manages the amount of discussion history provided on entering a + * room. + * + * @return the History that manages the amount of discussion history provided on entering a + * room. + */ + MUCInitialPresence.History getMUCHistory() { + // Return null if the history was not properly configured + if (!isConfigured()) { + return null; + } + + MUCInitialPresence.History mucHistory = new MUCInitialPresence.History(); + if (maxChars > -1) { + mucHistory.setMaxChars(maxChars); + } + if (maxStanzas > -1) { + mucHistory.setMaxStanzas(maxStanzas); + } + if (seconds > -1) { + mucHistory.setSeconds(seconds); + } + if (since != null) { + mucHistory.setSince(since); + } + return mucHistory; + } +} diff --git a/src/org/jivesoftware/smackx/muc/HostedRoom.java b/src/org/jivesoftware/smackx/muc/HostedRoom.java new file mode 100644 index 0000000..7cd580b --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/HostedRoom.java @@ -0,0 +1,65 @@ +/** + * $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.smackx.muc; + +import org.jivesoftware.smackx.packet.DiscoverItems; + +/** + * Hosted rooms by a chat service may be discovered if they are configured to appear in the room + * directory . The information that may be discovered is the XMPP address of the room and the room + * name. The address of the room may be used for obtaining more detailed information + * {@link org.jivesoftware.smackx.muc.MultiUserChat#getRoomInfo(org.jivesoftware.smack.Connection, String)} + * or could be used for joining the room + * {@link org.jivesoftware.smackx.muc.MultiUserChat#MultiUserChat(org.jivesoftware.smack.Connection, String)} + * and {@link org.jivesoftware.smackx.muc.MultiUserChat#join(String)}. + * + * @author Gaston Dombiak + */ +public class HostedRoom { + + private String jid; + + private String name; + + public HostedRoom(DiscoverItems.Item item) { + super(); + jid = item.getEntityID(); + name = item.getName(); + } + + /** + * Returns the XMPP address of the hosted room by the chat service. This address may be used + * when creating a <code>MultiUserChat</code> when joining a room. + * + * @return the XMPP address of the hosted room by the chat service. + */ + public String getJid() { + return jid; + } + + /** + * Returns the name of the room. + * + * @return the name of the room. + */ + public String getName() { + return name; + } +} diff --git a/src/org/jivesoftware/smackx/muc/InvitationListener.java b/src/org/jivesoftware/smackx/muc/InvitationListener.java new file mode 100644 index 0000000..34c915d --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/InvitationListener.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.smackx.muc; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.packet.Message; + +/** + * A listener that is fired anytime an invitation to join a MUC room is received. + * + * @author Gaston Dombiak + */ +public interface InvitationListener { + + /** + * Called when the an invitation to join a MUC room is received.<p> + * + * If the room is password-protected, the invitee will receive a password to use to join + * the room. If the room is members-only, the the invitee may be added to the member list. + * + * @param conn the Connection that received the invitation. + * @param room the room that invitation refers to. + * @param inviter the inviter that sent the invitation. (e.g. crone1@shakespeare.lit). + * @param reason the reason why the inviter sent the invitation. + * @param password the password to use when joining the room. + * @param message the message used by the inviter to send the invitation. + */ + public abstract void invitationReceived(Connection conn, String room, String inviter, String reason, + String password, Message message); + +} diff --git a/src/org/jivesoftware/smackx/muc/InvitationRejectionListener.java b/src/org/jivesoftware/smackx/muc/InvitationRejectionListener.java new file mode 100644 index 0000000..1580c6f --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/InvitationRejectionListener.java @@ -0,0 +1,38 @@ +/** + * $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.smackx.muc; + +/** + * A listener that is fired anytime an invitee declines or rejects an invitation. + * + * @author Gaston Dombiak + */ +public interface InvitationRejectionListener { + + /** + * Called when the invitee declines the invitation. + * + * @param invitee the invitee that declined the invitation. (e.g. hecate@shakespeare.lit). + * @param reason the reason why the invitee declined the invitation. + */ + public abstract void invitationDeclined(String invitee, String reason); + +} diff --git a/src/org/jivesoftware/smackx/muc/MultiUserChat.java b/src/org/jivesoftware/smackx/muc/MultiUserChat.java new file mode 100644 index 0000000..9a46444 --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/MultiUserChat.java @@ -0,0 +1,2743 @@ +/** + * $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.smackx.muc; + +import java.lang.ref.WeakReference; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + +import org.jivesoftware.smack.Chat; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.MessageListener; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.PacketInterceptor; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.FromMatchesFilter; +import org.jivesoftware.smack.filter.MessageTypeFilter; +import org.jivesoftware.smack.filter.PacketExtensionFilter; +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.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.smackx.Form; +import org.jivesoftware.smackx.NodeInformationProvider; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverItems; +import org.jivesoftware.smackx.packet.MUCAdmin; +import org.jivesoftware.smackx.packet.MUCInitialPresence; +import org.jivesoftware.smackx.packet.MUCOwner; +import org.jivesoftware.smackx.packet.MUCUser; + +/** + * A MultiUserChat is a conversation that takes place among many users in a virtual + * room. A room could have many occupants with different affiliation and roles. + * Possible affiliatons are "owner", "admin", "member", and "outcast". Possible roles + * are "moderator", "participant", and "visitor". Each role and affiliation guarantees + * different privileges (e.g. Send messages to all occupants, Kick participants and visitors, + * Grant voice, Edit member list, etc.). + * + * @author Gaston Dombiak, Larry Kirschner + */ +public class MultiUserChat { + + private final static String discoNamespace = "http://jabber.org/protocol/muc"; + private final static String discoNode = "http://jabber.org/protocol/muc#rooms"; + + private static Map<Connection, List<String>> joinedRooms = + new WeakHashMap<Connection, List<String>>(); + + private Connection connection; + private String room; + private String subject; + private String nickname = null; + private boolean joined = false; + private Map<String, Presence> occupantsMap = new ConcurrentHashMap<String, Presence>(); + + private final List<InvitationRejectionListener> invitationRejectionListeners = + new ArrayList<InvitationRejectionListener>(); + private final List<SubjectUpdatedListener> subjectUpdatedListeners = + new ArrayList<SubjectUpdatedListener>(); + private final List<UserStatusListener> userStatusListeners = + new ArrayList<UserStatusListener>(); + private final List<ParticipantStatusListener> participantStatusListeners = + new ArrayList<ParticipantStatusListener>(); + + private PacketFilter presenceFilter; + private List<PacketInterceptor> presenceInterceptors = new ArrayList<PacketInterceptor>(); + private PacketFilter messageFilter; + private RoomListenerMultiplexor roomListenerMultiplexor; + private ConnectionDetachedPacketCollector messageCollector; + private List<PacketListener> connectionListeners = new ArrayList<PacketListener>(); + + static { + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(final Connection connection) { + // Set on every established connection that this client supports the Multi-User + // Chat protocol. This information will be used when another client tries to + // discover whether this client supports MUC or not. + ServiceDiscoveryManager.getInstanceFor(connection).addFeature(discoNamespace); + // Set the NodeInformationProvider that will provide information about the + // joined rooms whenever a disco request is received + ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider( + discoNode, + new NodeInformationProvider() { + public List<DiscoverItems.Item> getNodeItems() { + List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>(); + Iterator<String> rooms=MultiUserChat.getJoinedRooms(connection); + while (rooms.hasNext()) { + answer.add(new DiscoverItems.Item(rooms.next())); + } + return answer; + } + + public List<String> getNodeFeatures() { + return null; + } + + public List<DiscoverInfo.Identity> getNodeIdentities() { + return null; + } + + @Override + public List<PacketExtension> getNodePacketExtensions() { + return null; + } + }); + } + }); + } + + /** + * Creates a new multi user chat with the specified connection and room name. Note: no + * information is sent to or received from the server until you attempt to + * {@link #join(String) join} the chat room. On some server implementations, + * the room will not be created until the first person joins it.<p> + * + * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com + * for the XMPP server example.com). You must ensure that the room address you're + * trying to connect to includes the proper chat sub-domain. + * + * @param connection the XMPP connection. + * @param room the name of the room in the form "roomName@service", where + * "service" is the hostname at which the multi-user chat + * service is running. Make sure to provide a valid JID. + */ + public MultiUserChat(Connection connection, String room) { + this.connection = connection; + this.room = room.toLowerCase(); + init(); + } + + /** + * Returns true if the specified user supports the Multi-User Chat protocol. + * + * @param connection the connection to use to perform the service discovery. + * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com. + * @return a boolean indicating whether the specified user supports the MUC protocol. + */ + public static boolean isServiceEnabled(Connection connection, String user) { + try { + DiscoverInfo result = + ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(user); + return result.containsFeature(discoNamespace); + } + catch (XMPPException e) { + e.printStackTrace(); + return false; + } + } + + /** + * Returns an Iterator on the rooms where the user has joined using a given connection. + * The Iterator will contain Strings where each String represents a room + * (e.g. room@muc.jabber.org). + * + * @param connection the connection used to join the rooms. + * @return an Iterator on the rooms where the user has joined using a given connection. + */ + private static Iterator<String> getJoinedRooms(Connection connection) { + List<String> rooms = joinedRooms.get(connection); + if (rooms != null) { + return rooms.iterator(); + } + // Return an iterator on an empty collection (i.e. the user never joined a room) + return new ArrayList<String>().iterator(); + } + + /** + * Returns an Iterator on the rooms where the requested user has joined. The Iterator will + * contain Strings where each String represents a room (e.g. room@muc.jabber.org). + * + * @param connection the connection to use to perform the service discovery. + * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com. + * @return an Iterator on the rooms where the requested user has joined. + */ + public static Iterator<String> getJoinedRooms(Connection connection, String user) { + try { + ArrayList<String> answer = new ArrayList<String>(); + // Send the disco packet to the user + DiscoverItems result = + ServiceDiscoveryManager.getInstanceFor(connection).discoverItems(user, discoNode); + // Collect the entityID for each returned item + for (Iterator<DiscoverItems.Item> items=result.getItems(); items.hasNext();) { + answer.add(items.next().getEntityID()); + } + return answer.iterator(); + } + catch (XMPPException e) { + e.printStackTrace(); + // Return an iterator on an empty collection + return new ArrayList<String>().iterator(); + } + } + + /** + * Returns the discovered information of a given room without actually having to join the room. + * The server will provide information only for rooms that are public. + * + * @param connection the XMPP connection to use for discovering information about the room. + * @param room the name of the room in the form "roomName@service" of which we want to discover + * its information. + * @return the discovered information of a given room without actually having to join the room. + * @throws XMPPException if an error occured while trying to discover information of a room. + */ + public static RoomInfo getRoomInfo(Connection connection, String room) + throws XMPPException { + DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(room); + return new RoomInfo(info); + } + + /** + * Returns a collection with the XMPP addresses of the Multi-User Chat services. + * + * @param connection the XMPP connection to use for discovering Multi-User Chat services. + * @return a collection with the XMPP addresses of the Multi-User Chat services. + * @throws XMPPException if an error occured while trying to discover MUC services. + */ + public static Collection<String> getServiceNames(Connection connection) throws XMPPException { + final List<String> answer = new ArrayList<String>(); + ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection); + DiscoverItems items = discoManager.discoverItems(connection.getServiceName()); + for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) { + DiscoverItems.Item item = it.next(); + try { + DiscoverInfo info = discoManager.discoverInfo(item.getEntityID()); + if (info.containsFeature("http://jabber.org/protocol/muc")) { + answer.add(item.getEntityID()); + } + } + catch (XMPPException e) { + // Trouble finding info in some cases. This is a workaround for + // discovering info on remote servers. + } + } + return answer; + } + + /** + * Returns a collection of HostedRooms where each HostedRoom has the XMPP address of the room + * and the room's name. Once discovered the rooms hosted by a chat service it is possible to + * discover more detailed room information or join the room. + * + * @param connection the XMPP connection to use for discovering hosted rooms by the MUC service. + * @param serviceName the service that is hosting the rooms to discover. + * @return a collection of HostedRooms. + * @throws XMPPException if an error occured while trying to discover the information. + */ + public static Collection<HostedRoom> getHostedRooms(Connection connection, String serviceName) + throws XMPPException { + List<HostedRoom> answer = new ArrayList<HostedRoom>(); + ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection); + DiscoverItems items = discoManager.discoverItems(serviceName); + for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) { + answer.add(new HostedRoom(it.next())); + } + return answer; + } + + /** + * Returns the name of the room this MultiUserChat object represents. + * + * @return the multi user chat room name. + */ + public String getRoom() { + return room; + } + + /** + * Creates the room according to some default configuration, assign the requesting user + * as the room owner, and add the owner to the room but not allow anyone else to enter + * the room (effectively "locking" the room). The requesting user will join the room + * under the specified nickname as soon as the room has been created.<p> + * + * To create an "Instant Room", that means a room with some default configuration that is + * available for immediate access, the room's owner should send an empty form after creating + * the room. {@link #sendConfigurationForm(Form)}<p> + * + * To create a "Reserved Room", that means a room manually configured by the room creator + * before anyone is allowed to enter, the room's owner should complete and send a form after + * creating the room. Once the completed configutation form is sent to the server, the server + * will unlock the room. {@link #sendConfigurationForm(Form)} + * + * @param nickname the nickname to use. + * @throws XMPPException if the room couldn't be created for some reason + * (e.g. room already exists; user already joined to an existant room or + * 405 error if the user is not allowed to create the room) + */ + public synchronized void create(String nickname) throws XMPPException { + if (nickname == null || nickname.equals("")) { + throw new IllegalArgumentException("Nickname must not be null or blank."); + } + // If we've already joined the room, leave it before joining under a new + // nickname. + if (joined) { + throw new IllegalStateException("Creation failed - User already joined the room."); + } + // We create a room by sending a presence packet to room@service/nick + // and signal support for MUC. The owner will be automatically logged into the room. + Presence joinPresence = new Presence(Presence.Type.available); + joinPresence.setTo(room + "/" + nickname); + // Indicate the the client supports MUC + joinPresence.addExtension(new MUCInitialPresence()); + // Invoke presence interceptors so that extra information can be dynamically added + for (PacketInterceptor packetInterceptor : presenceInterceptors) { + packetInterceptor.interceptPacket(joinPresence); + } + + // Wait for a presence packet back from the server. + PacketFilter responseFilter = + new AndFilter( + new FromMatchesFilter(room + "/" + nickname), + new PacketTypeFilter(Presence.class)); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send create & join packet. + connection.sendPacket(joinPresence); + // Wait up to a certain number of seconds for a reply. + Presence presence = + (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (presence == null) { + throw new XMPPException("No response from server."); + } + else if (presence.getError() != null) { + throw new XMPPException(presence.getError()); + } + // Whether the room existed before or was created, the user has joined the room + this.nickname = nickname; + joined = true; + userHasJoined(); + + // Look for confirmation of room creation from the server + MUCUser mucUser = getMUCUserExtension(presence); + if (mucUser != null && mucUser.getStatus() != null) { + if ("201".equals(mucUser.getStatus().getCode())) { + // Room was created and the user has joined the room + return; + } + } + // We need to leave the room since it seems that the room already existed + leave(); + throw new XMPPException("Creation failed - Missing acknowledge of room creation."); + } + + /** + * Joins the chat room using the specified nickname. If already joined + * using another nickname, this method will first leave the room and then + * re-join using the new nickname. The default timeout of Smack for a reply + * from the group chat server that the join succeeded will be used. After + * joining the room, the room will decide the amount of history to send. + * + * @param nickname the nickname to use. + * @throws XMPPException if an error occurs joining the room. In particular, a + * 401 error can occur if no password was provided and one is required; or a + * 403 error can occur if the user is banned; or a + * 404 error can occur if the room does not exist or is locked; or a + * 407 error can occur if user is not on the member list; or a + * 409 error can occur if someone is already in the group chat with the same nickname. + */ + public void join(String nickname) throws XMPPException { + join(nickname, null, null, SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Joins the chat room using the specified nickname and password. If already joined + * using another nickname, this method will first leave the room and then + * re-join using the new nickname. The default timeout of Smack for a reply + * from the group chat server that the join succeeded will be used. After + * joining the room, the room will decide the amount of history to send.<p> + * + * A password is required when joining password protected rooms. If the room does + * not require a password there is no need to provide one. + * + * @param nickname the nickname to use. + * @param password the password to use. + * @throws XMPPException if an error occurs joining the room. In particular, a + * 401 error can occur if no password was provided and one is required; or a + * 403 error can occur if the user is banned; or a + * 404 error can occur if the room does not exist or is locked; or a + * 407 error can occur if user is not on the member list; or a + * 409 error can occur if someone is already in the group chat with the same nickname. + */ + public void join(String nickname, String password) throws XMPPException { + join(nickname, password, null, SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Joins the chat room using the specified nickname and password. If already joined + * using another nickname, this method will first leave the room and then + * re-join using the new nickname.<p> + * + * To control the amount of history to receive while joining a room you will need to provide + * a configured DiscussionHistory object.<p> + * + * A password is required when joining password protected rooms. If the room does + * not require a password there is no need to provide one.<p> + * + * If the room does not already exist when the user seeks to enter it, the server will + * decide to create a new room or not. + * + * @param nickname the nickname to use. + * @param password the password to use. + * @param history the amount of discussion history to receive while joining a room. + * @param timeout the amount of time to wait for a reply from the MUC service(in milleseconds). + * @throws XMPPException if an error occurs joining the room. In particular, a + * 401 error can occur if no password was provided and one is required; or a + * 403 error can occur if the user is banned; or a + * 404 error can occur if the room does not exist or is locked; or a + * 407 error can occur if user is not on the member list; or a + * 409 error can occur if someone is already in the group chat with the same nickname. + */ + public synchronized void join( + String nickname, + String password, + DiscussionHistory history, + long timeout) + throws XMPPException { + if (nickname == null || nickname.equals("")) { + throw new IllegalArgumentException("Nickname must not be null or blank."); + } + // If we've already joined the room, leave it before joining under a new + // nickname. + if (joined) { + leave(); + } + // We join a room by sending a presence packet where the "to" + // field is in the form "roomName@service/nickname" + Presence joinPresence = new Presence(Presence.Type.available); + joinPresence.setTo(room + "/" + nickname); + + // Indicate the the client supports MUC + MUCInitialPresence mucInitialPresence = new MUCInitialPresence(); + if (password != null) { + mucInitialPresence.setPassword(password); + } + if (history != null) { + mucInitialPresence.setHistory(history.getMUCHistory()); + } + joinPresence.addExtension(mucInitialPresence); + // Invoke presence interceptors so that extra information can be dynamically added + for (PacketInterceptor packetInterceptor : presenceInterceptors) { + packetInterceptor.interceptPacket(joinPresence); + } + + // Wait for a presence packet back from the server. + PacketFilter responseFilter = + new AndFilter( + new FromMatchesFilter(room + "/" + nickname), + new PacketTypeFilter(Presence.class)); + PacketCollector response = null; + Presence presence; + try { + response = connection.createPacketCollector(responseFilter); + // Send join packet. + connection.sendPacket(joinPresence); + // Wait up to a certain number of seconds for a reply. + presence = (Presence) response.nextResult(timeout); + } + finally { + // Stop queuing results + if (response != null) { + response.cancel(); + } + } + + if (presence == null) { + throw new XMPPException("No response from server."); + } + else if (presence.getError() != null) { + throw new XMPPException(presence.getError()); + } + this.nickname = nickname; + joined = true; + userHasJoined(); + } + + /** + * Returns true if currently in the multi user chat (after calling the {@link + * #join(String)} method). + * + * @return true if currently in the multi user chat room. + */ + public boolean isJoined() { + return joined; + } + + /** + * Leave the chat room. + */ + public synchronized void leave() { + // If not joined already, do nothing. + if (!joined) { + return; + } + // We leave a room by sending a presence packet where the "to" + // field is in the form "roomName@service/nickname" + Presence leavePresence = new Presence(Presence.Type.unavailable); + leavePresence.setTo(room + "/" + nickname); + // Invoke presence interceptors so that extra information can be dynamically added + for (PacketInterceptor packetInterceptor : presenceInterceptors) { + packetInterceptor.interceptPacket(leavePresence); + } + connection.sendPacket(leavePresence); + // Reset occupant information. + occupantsMap.clear(); + nickname = null; + joined = false; + userHasLeft(); + } + + /** + * Returns the room's configuration form that the room's owner can use or <tt>null</tt> if + * no configuration is possible. The configuration form allows to set the room's language, + * enable logging, specify room's type, etc.. + * + * @return the Form that contains the fields to complete together with the instrucions or + * <tt>null</tt> if no configuration is possible. + * @throws XMPPException if an error occurs asking the configuration form for the room. + */ + public Form getConfigurationForm() throws XMPPException { + MUCOwner iq = new MUCOwner(); + iq.setTo(room); + iq.setType(IQ.Type.GET); + + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Request the configuration form to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + return Form.getFormFrom(answer); + } + + /** + * Sends the completed configuration form to the server. The room will be configured + * with the new settings defined in the form. If the form is empty then the server + * will create an instant room (will use default configuration). + * + * @param form the form with the new settings. + * @throws XMPPException if an error occurs setting the new rooms' configuration. + */ + public void sendConfigurationForm(Form form) throws XMPPException { + MUCOwner iq = new MUCOwner(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + iq.addExtension(form.getDataFormToSend()); + + // Filter packets looking for an answer from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the completed configuration form to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + /** + * Returns the room's registration form that an unaffiliated user, can use to become a member + * of the room or <tt>null</tt> if no registration is possible. Some rooms may restrict the + * privilege to register members and allow only room admins to add new members.<p> + * + * If the user requesting registration requirements is not allowed to register with the room + * (e.g. because that privilege has been restricted), the room will return a "Not Allowed" + * error to the user (error code 405). + * + * @return the registration Form that contains the fields to complete together with the + * instrucions or <tt>null</tt> if no registration is possible. + * @throws XMPPException if an error occurs asking the registration form for the room or a + * 405 error if the user is not allowed to register with the room. + */ + public Form getRegistrationForm() throws XMPPException { + Registration reg = new Registration(); + reg.setType(IQ.Type.GET); + reg.setTo(room); + + PacketFilter filter = + new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class)); + PacketCollector collector = connection.createPacketCollector(filter); + connection.sendPacket(reg); + IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from server."); + } + else if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + return Form.getFormFrom(result); + } + + /** + * Sends the completed registration form to the server. After the user successfully submits + * the form, the room may queue the request for review by the room admins or may immediately + * add the user to the member list by changing the user's affiliation from "none" to "member.<p> + * + * If the desired room nickname is already reserved for that room, the room will return a + * "Conflict" error to the user (error code 409). If the room does not support registration, + * it will return a "Service Unavailable" error to the user (error code 503). + * + * @param form the completed registration form. + * @throws XMPPException if an error occurs submitting the registration form. In particular, a + * 409 error can occur if the desired room nickname is already reserved for that room; + * or a 503 error can occur if the room does not support registration. + */ + public void sendRegistrationForm(Form form) throws XMPPException { + Registration reg = new Registration(); + reg.setType(IQ.Type.SET); + reg.setTo(room); + reg.addExtension(form.getDataFormToSend()); + + PacketFilter filter = + new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class)); + PacketCollector collector = connection.createPacketCollector(filter); + connection.sendPacket(reg); + IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (result == null) { + throw new XMPPException("No response from server."); + } + else if (result.getType() == IQ.Type.ERROR) { + throw new XMPPException(result.getError()); + } + } + + /** + * Sends a request to the server to destroy the room. The sender of the request + * should be the room's owner. If the sender of the destroy request is not the room's owner + * then the server will answer a "Forbidden" error (403). + * + * @param reason the reason for the room destruction. + * @param alternateJID the JID of an alternate location. + * @throws XMPPException if an error occurs while trying to destroy the room. + * An error can occur which will be wrapped by an XMPPException -- + * XMPP error code 403. The error code can be used to present more + * appropiate error messages to end-users. + */ + public void destroy(String reason, String alternateJID) throws XMPPException { + MUCOwner iq = new MUCOwner(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + + // Create the reason for the room destruction + MUCOwner.Destroy destroy = new MUCOwner.Destroy(); + destroy.setReason(reason); + destroy.setJid(alternateJID); + iq.setDestroy(destroy); + + // Wait for a presence packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the room destruction request. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + // Reset occupant information. + occupantsMap.clear(); + nickname = null; + joined = false; + userHasLeft(); + } + + /** + * Invites another user to the room in which one is an occupant. The invitation + * will be sent to the room which in turn will forward the invitation to the invitee.<p> + * + * If the room is password-protected, the invitee will receive a password to use to join + * the room. If the room is members-only, the the invitee may be added to the member list. + * + * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit) + * @param reason the reason why the user is being invited. + */ + public void invite(String user, String reason) { + invite(new Message(), user, reason); + } + + /** + * Invites another user to the room in which one is an occupant using a given Message. The invitation + * will be sent to the room which in turn will forward the invitation to the invitee.<p> + * + * If the room is password-protected, the invitee will receive a password to use to join + * the room. If the room is members-only, the the invitee may be added to the member list. + * + * @param message the message to use for sending the invitation. + * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit) + * @param reason the reason why the user is being invited. + */ + public void invite(Message message, String user, String reason) { + // TODO listen for 404 error code when inviter supplies a non-existent JID + message.setTo(room); + + // Create the MUCUser packet that will include the invitation + MUCUser mucUser = new MUCUser(); + MUCUser.Invite invite = new MUCUser.Invite(); + invite.setTo(user); + invite.setReason(reason); + mucUser.setInvite(invite); + // Add the MUCUser packet that includes the invitation to the message + message.addExtension(mucUser); + + connection.sendPacket(message); + } + + /** + * Informs the sender of an invitation that the invitee declines the invitation. The rejection + * will be sent to the room which in turn will forward the rejection to the inviter. + * + * @param conn the connection to use for sending the rejection. + * @param room the room that sent the original invitation. + * @param inviter the inviter of the declined invitation. + * @param reason the reason why the invitee is declining the invitation. + */ + public static void decline(Connection conn, String room, String inviter, String reason) { + Message message = new Message(room); + + // Create the MUCUser packet that will include the rejection + MUCUser mucUser = new MUCUser(); + MUCUser.Decline decline = new MUCUser.Decline(); + decline.setTo(inviter); + decline.setReason(reason); + mucUser.setDecline(decline); + // Add the MUCUser packet that includes the rejection + message.addExtension(mucUser); + + conn.sendPacket(message); + } + + /** + * Adds a listener to invitation notifications. The listener will be fired anytime + * an invitation is received. + * + * @param conn the connection where the listener will be applied. + * @param listener an invitation listener. + */ + public static void addInvitationListener(Connection conn, InvitationListener listener) { + InvitationsMonitor.getInvitationsMonitor(conn).addInvitationListener(listener); + } + + /** + * Removes a listener to invitation notifications. The listener will be fired anytime + * an invitation is received. + * + * @param conn the connection where the listener was applied. + * @param listener an invitation listener. + */ + public static void removeInvitationListener(Connection conn, InvitationListener listener) { + InvitationsMonitor.getInvitationsMonitor(conn).removeInvitationListener(listener); + } + + /** + * Adds a listener to invitation rejections notifications. The listener will be fired anytime + * an invitation is declined. + * + * @param listener an invitation rejection listener. + */ + public void addInvitationRejectionListener(InvitationRejectionListener listener) { + synchronized (invitationRejectionListeners) { + if (!invitationRejectionListeners.contains(listener)) { + invitationRejectionListeners.add(listener); + } + } + } + + /** + * Removes a listener from invitation rejections notifications. The listener will be fired + * anytime an invitation is declined. + * + * @param listener an invitation rejection listener. + */ + public void removeInvitationRejectionListener(InvitationRejectionListener listener) { + synchronized (invitationRejectionListeners) { + invitationRejectionListeners.remove(listener); + } + } + + /** + * Fires invitation rejection listeners. + * + * @param invitee the user being invited. + * @param reason the reason for the rejection + */ + private void fireInvitationRejectionListeners(String invitee, String reason) { + InvitationRejectionListener[] listeners; + synchronized (invitationRejectionListeners) { + listeners = new InvitationRejectionListener[invitationRejectionListeners.size()]; + invitationRejectionListeners.toArray(listeners); + } + for (InvitationRejectionListener listener : listeners) { + listener.invitationDeclined(invitee, reason); + } + } + + /** + * Adds a listener to subject change notifications. The listener will be fired anytime + * the room's subject changes. + * + * @param listener a subject updated listener. + */ + public void addSubjectUpdatedListener(SubjectUpdatedListener listener) { + synchronized (subjectUpdatedListeners) { + if (!subjectUpdatedListeners.contains(listener)) { + subjectUpdatedListeners.add(listener); + } + } + } + + /** + * Removes a listener from subject change notifications. The listener will be fired + * anytime the room's subject changes. + * + * @param listener a subject updated listener. + */ + public void removeSubjectUpdatedListener(SubjectUpdatedListener listener) { + synchronized (subjectUpdatedListeners) { + subjectUpdatedListeners.remove(listener); + } + } + + /** + * Fires subject updated listeners. + */ + private void fireSubjectUpdatedListeners(String subject, String from) { + SubjectUpdatedListener[] listeners; + synchronized (subjectUpdatedListeners) { + listeners = new SubjectUpdatedListener[subjectUpdatedListeners.size()]; + subjectUpdatedListeners.toArray(listeners); + } + for (SubjectUpdatedListener listener : listeners) { + listener.subjectUpdated(subject, from); + } + } + + /** + * Adds a new {@link PacketInterceptor} that will be invoked every time a new presence + * is going to be sent by this MultiUserChat to the server. Packet interceptors may + * add new extensions to the presence that is going to be sent to the MUC service. + * + * @param presenceInterceptor the new packet interceptor that will intercept presence packets. + */ + public void addPresenceInterceptor(PacketInterceptor presenceInterceptor) { + presenceInterceptors.add(presenceInterceptor); + } + + /** + * Removes a {@link PacketInterceptor} that was being invoked every time a new presence + * was being sent by this MultiUserChat to the server. Packet interceptors may + * add new extensions to the presence that is going to be sent to the MUC service. + * + * @param presenceInterceptor the packet interceptor to remove. + */ + public void removePresenceInterceptor(PacketInterceptor presenceInterceptor) { + presenceInterceptors.remove(presenceInterceptor); + } + + /** + * Returns the last known room's subject or <tt>null</tt> if the user hasn't joined the room + * or the room does not have a subject yet. In case the room has a subject, as soon as the + * user joins the room a message with the current room's subject will be received.<p> + * + * To be notified every time the room's subject change you should add a listener + * to this room. {@link #addSubjectUpdatedListener(SubjectUpdatedListener)}<p> + * + * To change the room's subject use {@link #changeSubject(String)}. + * + * @return the room's subject or <tt>null</tt> if the user hasn't joined the room or the + * room does not have a subject yet. + */ + public String getSubject() { + return subject; + } + + /** + * Returns the reserved room nickname for the user in the room. A user may have a reserved + * nickname, for example through explicit room registration or database integration. In such + * cases it may be desirable for the user to discover the reserved nickname before attempting + * to enter the room. + * + * @return the reserved room nickname or <tt>null</tt> if none. + */ + public String getReservedNickname() { + try { + DiscoverInfo result = + ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo( + room, + "x-roomuser-item"); + // Look for an Identity that holds the reserved nickname and return its name + for (Iterator<DiscoverInfo.Identity> identities = result.getIdentities(); + identities.hasNext();) { + DiscoverInfo.Identity identity = identities.next(); + return identity.getName(); + } + // If no Identity was found then the user does not have a reserved room nickname + return null; + } + catch (XMPPException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Returns the nickname that was used to join the room, or <tt>null</tt> if not + * currently joined. + * + * @return the nickname currently being used. + */ + public String getNickname() { + return nickname; + } + + /** + * Changes the occupant's nickname to a new nickname within the room. Each room occupant + * will receive two presence packets. One of type "unavailable" for the old nickname and one + * indicating availability for the new nickname. The unavailable presence will contain the new + * nickname and an appropriate status code (namely 303) as extended presence information. The + * status code 303 indicates that the occupant is changing his/her nickname. + * + * @param nickname the new nickname within the room. + * @throws XMPPException if the new nickname is already in use by another occupant. + */ + public void changeNickname(String nickname) throws XMPPException { + if (nickname == null || nickname.equals("")) { + throw new IllegalArgumentException("Nickname must not be null or blank."); + } + // Check that we already have joined the room before attempting to change the + // nickname. + if (!joined) { + throw new IllegalStateException("Must be logged into the room to change nickname."); + } + // We change the nickname by sending a presence packet where the "to" + // field is in the form "roomName@service/nickname" + // We don't have to signal the MUC support again + Presence joinPresence = new Presence(Presence.Type.available); + joinPresence.setTo(room + "/" + nickname); + // Invoke presence interceptors so that extra information can be dynamically added + for (PacketInterceptor packetInterceptor : presenceInterceptors) { + packetInterceptor.interceptPacket(joinPresence); + } + + // Wait for a presence packet back from the server. + PacketFilter responseFilter = + new AndFilter( + new FromMatchesFilter(room + "/" + nickname), + new PacketTypeFilter(Presence.class)); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send join packet. + connection.sendPacket(joinPresence); + // Wait up to a certain number of seconds for a reply. + Presence presence = + (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (presence == null) { + throw new XMPPException("No response from server."); + } + else if (presence.getError() != null) { + throw new XMPPException(presence.getError()); + } + this.nickname = nickname; + } + + /** + * Changes the occupant's availability status within the room. The presence type + * will remain available but with a new status that describes the presence update and + * a new presence mode (e.g. Extended away). + * + * @param status a text message describing the presence update. + * @param mode the mode type for the presence update. + */ + public void changeAvailabilityStatus(String status, Presence.Mode mode) { + if (nickname == null || nickname.equals("")) { + throw new IllegalArgumentException("Nickname must not be null or blank."); + } + // Check that we already have joined the room before attempting to change the + // availability status. + if (!joined) { + throw new IllegalStateException( + "Must be logged into the room to change the " + "availability status."); + } + // We change the availability status by sending a presence packet to the room with the + // new presence status and mode + Presence joinPresence = new Presence(Presence.Type.available); + joinPresence.setStatus(status); + joinPresence.setMode(mode); + joinPresence.setTo(room + "/" + nickname); + // Invoke presence interceptors so that extra information can be dynamically added + for (PacketInterceptor packetInterceptor : presenceInterceptors) { + packetInterceptor.interceptPacket(joinPresence); + } + + // Send join packet. + connection.sendPacket(joinPresence); + } + + /** + * Kicks a visitor or participant from the room. The kicked occupant will receive a presence + * of type "unavailable" including a status code 307 and optionally along with the reason + * (if provided) and the bare JID of the user who initiated the kick. After the occupant + * was kicked from the room, the rest of the occupants will receive a presence of type + * "unavailable". The presence will include a status code 307 which means that the occupant + * was kicked from the room. + * + * @param nickname the nickname of the participant or visitor to kick from the room + * (e.g. "john"). + * @param reason the reason why the participant or visitor is being kicked from the room. + * @throws XMPPException if an error occurs kicking the occupant. In particular, a + * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin" + * was intended to be kicked (i.e. Not Allowed error); or a + * 403 error can occur if the occupant that intended to kick another occupant does + * not have kicking privileges (i.e. Forbidden error); or a + * 400 error can occur if the provided nickname is not present in the room. + */ + public void kickParticipant(String nickname, String reason) throws XMPPException { + changeRole(nickname, "none", reason); + } + + /** + * Grants voice to visitors in the room. In a moderated room, a moderator may want to manage + * who does and does not have "voice" in the room. To have voice means that a room occupant + * is able to send messages to the room occupants. + * + * @param nicknames the nicknames of the visitors to grant voice in the room (e.g. "john"). + * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a + * 403 error can occur if the occupant that intended to grant voice is not + * a moderator in this room (i.e. Forbidden error); or a + * 400 error can occur if the provided nickname is not present in the room. + */ + public void grantVoice(Collection<String> nicknames) throws XMPPException { + changeRole(nicknames, "participant"); + } + + /** + * Grants voice to a visitor in the room. In a moderated room, a moderator may want to manage + * who does and does not have "voice" in the room. To have voice means that a room occupant + * is able to send messages to the room occupants. + * + * @param nickname the nickname of the visitor to grant voice in the room (e.g. "john"). + * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a + * 403 error can occur if the occupant that intended to grant voice is not + * a moderator in this room (i.e. Forbidden error); or a + * 400 error can occur if the provided nickname is not present in the room. + */ + public void grantVoice(String nickname) throws XMPPException { + changeRole(nickname, "participant", null); + } + + /** + * Revokes voice from participants in the room. In a moderated room, a moderator may want to + * revoke an occupant's privileges to speak. To have voice means that a room occupant + * is able to send messages to the room occupants. + * + * @param nicknames the nicknames of the participants to revoke voice (e.g. "john"). + * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a + * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin" + * was tried to revoke his voice (i.e. Not Allowed error); or a + * 400 error can occur if the provided nickname is not present in the room. + */ + public void revokeVoice(Collection<String> nicknames) throws XMPPException { + changeRole(nicknames, "visitor"); + } + + /** + * Revokes voice from a participant in the room. In a moderated room, a moderator may want to + * revoke an occupant's privileges to speak. To have voice means that a room occupant + * is able to send messages to the room occupants. + * + * @param nickname the nickname of the participant to revoke voice (e.g. "john"). + * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a + * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin" + * was tried to revoke his voice (i.e. Not Allowed error); or a + * 400 error can occur if the provided nickname is not present in the room. + */ + public void revokeVoice(String nickname) throws XMPPException { + changeRole(nickname, "visitor", null); + } + + /** + * Bans users from the room. An admin or owner of the room can ban users from a room. This + * means that the banned user will no longer be able to join the room unless the ban has been + * removed. If the banned user was present in the room then he/she will be removed from the + * room and notified that he/she was banned along with the reason (if provided) and the bare + * XMPP user ID of the user who initiated the ban. + * + * @param jids the bare XMPP user IDs of the users to ban. + * @throws XMPPException if an error occurs banning a user. In particular, a + * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin" + * was tried to be banned (i.e. Not Allowed error). + */ + public void banUsers(Collection<String> jids) throws XMPPException { + changeAffiliationByAdmin(jids, "outcast"); + } + + /** + * Bans a user from the room. An admin or owner of the room can ban users from a room. This + * means that the banned user will no longer be able to join the room unless the ban has been + * removed. If the banned user was present in the room then he/she will be removed from the + * room and notified that he/she was banned along with the reason (if provided) and the bare + * XMPP user ID of the user who initiated the ban. + * + * @param jid the bare XMPP user ID of the user to ban (e.g. "user@host.org"). + * @param reason the optional reason why the user was banned. + * @throws XMPPException if an error occurs banning a user. In particular, a + * 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin" + * was tried to be banned (i.e. Not Allowed error). + */ + public void banUser(String jid, String reason) throws XMPPException { + changeAffiliationByAdmin(jid, "outcast", reason); + } + + /** + * Grants membership to other users. Only administrators are able to grant membership. A user + * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room + * that a user cannot enter without being on the member list). + * + * @param jids the XMPP user IDs of the users to grant membership. + * @throws XMPPException if an error occurs granting membership to a user. + */ + public void grantMembership(Collection<String> jids) throws XMPPException { + changeAffiliationByAdmin(jids, "member"); + } + + /** + * Grants membership to a user. Only administrators are able to grant membership. A user + * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room + * that a user cannot enter without being on the member list). + * + * @param jid the XMPP user ID of the user to grant membership (e.g. "user@host.org"). + * @throws XMPPException if an error occurs granting membership to a user. + */ + public void grantMembership(String jid) throws XMPPException { + changeAffiliationByAdmin(jid, "member", null); + } + + /** + * Revokes users' membership. Only administrators are able to revoke membership. A user + * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room + * that a user cannot enter without being on the member list). If the user is in the room and + * the room is of type members-only then the user will be removed from the room. + * + * @param jids the bare XMPP user IDs of the users to revoke membership. + * @throws XMPPException if an error occurs revoking membership to a user. + */ + public void revokeMembership(Collection<String> jids) throws XMPPException { + changeAffiliationByAdmin(jids, "none"); + } + + /** + * Revokes a user's membership. Only administrators are able to revoke membership. A user + * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room + * that a user cannot enter without being on the member list). If the user is in the room and + * the room is of type members-only then the user will be removed from the room. + * + * @param jid the bare XMPP user ID of the user to revoke membership (e.g. "user@host.org"). + * @throws XMPPException if an error occurs revoking membership to a user. + */ + public void revokeMembership(String jid) throws XMPPException { + changeAffiliationByAdmin(jid, "none", null); + } + + /** + * Grants moderator privileges to participants or visitors. Room administrators may grant + * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite + * other users, modify room's subject plus all the partcipants privileges. + * + * @param nicknames the nicknames of the occupants to grant moderator privileges. + * @throws XMPPException if an error occurs granting moderator privileges to a user. + */ + public void grantModerator(Collection<String> nicknames) throws XMPPException { + changeRole(nicknames, "moderator"); + } + + /** + * Grants moderator privileges to a participant or visitor. Room administrators may grant + * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite + * other users, modify room's subject plus all the partcipants privileges. + * + * @param nickname the nickname of the occupant to grant moderator privileges. + * @throws XMPPException if an error occurs granting moderator privileges to a user. + */ + public void grantModerator(String nickname) throws XMPPException { + changeRole(nickname, "moderator", null); + } + + /** + * Revokes moderator privileges from other users. The occupant that loses moderator + * privileges will become a participant. Room administrators may revoke moderator privileges + * only to occupants whose affiliation is member or none. This means that an administrator is + * not allowed to revoke moderator privileges from other room administrators or owners. + * + * @param nicknames the nicknames of the occupants to revoke moderator privileges. + * @throws XMPPException if an error occurs revoking moderator privileges from a user. + */ + public void revokeModerator(Collection<String> nicknames) throws XMPPException { + changeRole(nicknames, "participant"); + } + + /** + * Revokes moderator privileges from another user. The occupant that loses moderator + * privileges will become a participant. Room administrators may revoke moderator privileges + * only to occupants whose affiliation is member or none. This means that an administrator is + * not allowed to revoke moderator privileges from other room administrators or owners. + * + * @param nickname the nickname of the occupant to revoke moderator privileges. + * @throws XMPPException if an error occurs revoking moderator privileges from a user. + */ + public void revokeModerator(String nickname) throws XMPPException { + changeRole(nickname, "participant", null); + } + + /** + * Grants ownership privileges to other users. Room owners may grant ownership privileges. + * Some room implementations will not allow to grant ownership privileges to other users. + * An owner is allowed to change defining room features as well as perform all administrative + * functions. + * + * @param jids the collection of bare XMPP user IDs of the users to grant ownership. + * @throws XMPPException if an error occurs granting ownership privileges to a user. + */ + public void grantOwnership(Collection<String> jids) throws XMPPException { + changeAffiliationByAdmin(jids, "owner"); + } + + /** + * Grants ownership privileges to another user. Room owners may grant ownership privileges. + * Some room implementations will not allow to grant ownership privileges to other users. + * An owner is allowed to change defining room features as well as perform all administrative + * functions. + * + * @param jid the bare XMPP user ID of the user to grant ownership (e.g. "user@host.org"). + * @throws XMPPException if an error occurs granting ownership privileges to a user. + */ + public void grantOwnership(String jid) throws XMPPException { + changeAffiliationByAdmin(jid, "owner", null); + } + + /** + * Revokes ownership privileges from other users. The occupant that loses ownership + * privileges will become an administrator. Room owners may revoke ownership privileges. + * Some room implementations will not allow to grant ownership privileges to other users. + * + * @param jids the bare XMPP user IDs of the users to revoke ownership. + * @throws XMPPException if an error occurs revoking ownership privileges from a user. + */ + public void revokeOwnership(Collection<String> jids) throws XMPPException { + changeAffiliationByAdmin(jids, "admin"); + } + + /** + * Revokes ownership privileges from another user. The occupant that loses ownership + * privileges will become an administrator. Room owners may revoke ownership privileges. + * Some room implementations will not allow to grant ownership privileges to other users. + * + * @param jid the bare XMPP user ID of the user to revoke ownership (e.g. "user@host.org"). + * @throws XMPPException if an error occurs revoking ownership privileges from a user. + */ + public void revokeOwnership(String jid) throws XMPPException { + changeAffiliationByAdmin(jid, "admin", null); + } + + /** + * Grants administrator privileges to other users. Room owners may grant administrator + * privileges to a member or unaffiliated user. An administrator is allowed to perform + * administrative functions such as banning users and edit moderator list. + * + * @param jids the bare XMPP user IDs of the users to grant administrator privileges. + * @throws XMPPException if an error occurs granting administrator privileges to a user. + */ + public void grantAdmin(Collection<String> jids) throws XMPPException { + changeAffiliationByOwner(jids, "admin"); + } + + /** + * Grants administrator privileges to another user. Room owners may grant administrator + * privileges to a member or unaffiliated user. An administrator is allowed to perform + * administrative functions such as banning users and edit moderator list. + * + * @param jid the bare XMPP user ID of the user to grant administrator privileges + * (e.g. "user@host.org"). + * @throws XMPPException if an error occurs granting administrator privileges to a user. + */ + public void grantAdmin(String jid) throws XMPPException { + changeAffiliationByOwner(jid, "admin"); + } + + /** + * Revokes administrator privileges from users. The occupant that loses administrator + * privileges will become a member. Room owners may revoke administrator privileges from + * a member or unaffiliated user. + * + * @param jids the bare XMPP user IDs of the user to revoke administrator privileges. + * @throws XMPPException if an error occurs revoking administrator privileges from a user. + */ + public void revokeAdmin(Collection<String> jids) throws XMPPException { + changeAffiliationByOwner(jids, "member"); + } + + /** + * Revokes administrator privileges from a user. The occupant that loses administrator + * privileges will become a member. Room owners may revoke administrator privileges from + * a member or unaffiliated user. + * + * @param jid the bare XMPP user ID of the user to revoke administrator privileges + * (e.g. "user@host.org"). + * @throws XMPPException if an error occurs revoking administrator privileges from a user. + */ + public void revokeAdmin(String jid) throws XMPPException { + changeAffiliationByOwner(jid, "member"); + } + + private void changeAffiliationByOwner(String jid, String affiliation) throws XMPPException { + MUCOwner iq = new MUCOwner(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + // Set the new affiliation. + MUCOwner.Item item = new MUCOwner.Item(affiliation); + item.setJid(jid); + iq.addItem(item); + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the change request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + private void changeAffiliationByOwner(Collection<String> jids, String affiliation) + throws XMPPException { + MUCOwner iq = new MUCOwner(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + for (String jid : jids) { + // Set the new affiliation. + MUCOwner.Item item = new MUCOwner.Item(affiliation); + item.setJid(jid); + iq.addItem(item); + } + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the change request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + /** + * Tries to change the affiliation with an 'muc#admin' namespace + * + * @param jid + * @param affiliation + * @param reason the reason for the affiliation change (optional) + * @throws XMPPException + */ + private void changeAffiliationByAdmin(String jid, String affiliation, String reason) + throws XMPPException { + MUCAdmin iq = new MUCAdmin(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + // Set the new affiliation. + MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null); + item.setJid(jid); + if(reason != null) + item.setReason(reason); + iq.addItem(item); + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the change request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + private void changeAffiliationByAdmin(Collection<String> jids, String affiliation) + throws XMPPException { + MUCAdmin iq = new MUCAdmin(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + for (String jid : jids) { + // Set the new affiliation. + MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null); + item.setJid(jid); + iq.addItem(item); + } + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the change request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + private void changeRole(String nickname, String role, String reason) throws XMPPException { + MUCAdmin iq = new MUCAdmin(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + // Set the new role. + MUCAdmin.Item item = new MUCAdmin.Item(null, role); + item.setNick(nickname); + item.setReason(reason); + iq.addItem(item); + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the change request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + private void changeRole(Collection<String> nicknames, String role) throws XMPPException { + MUCAdmin iq = new MUCAdmin(); + iq.setTo(room); + iq.setType(IQ.Type.SET); + for (String nickname : nicknames) { + // Set the new role. + MUCAdmin.Item item = new MUCAdmin.Item(null, role); + item.setNick(nickname); + iq.addItem(item); + } + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the change request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + /** + * Returns the number of occupants in the group chat.<p> + * + * Note: this value will only be accurate after joining the group chat, and + * may fluctuate over time. If you query this value directly after joining the + * group chat it may not be accurate, as it takes a certain amount of time for + * the server to send all presence packets to this client. + * + * @return the number of occupants in the group chat. + */ + public int getOccupantsCount() { + return occupantsMap.size(); + } + + /** + * Returns an Iterator (of Strings) for the list of fully qualified occupants + * in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser". + * Typically, a client would only display the nickname of the occupant. To + * get the nickname from the fully qualified name, use the + * {@link org.jivesoftware.smack.util.StringUtils#parseResource(String)} method. + * Note: this value will only be accurate after joining the group chat, and may + * fluctuate over time. + * + * @return an Iterator for the occupants in the group chat. + */ + public Iterator<String> getOccupants() { + return Collections.unmodifiableList(new ArrayList<String>(occupantsMap.keySet())) + .iterator(); + } + + /** + * Returns the presence info for a particular user, or <tt>null</tt> if the user + * is not in the room.<p> + * + * @param user the room occupant to search for his presence. The format of user must + * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch). + * @return the occupant's current presence, or <tt>null</tt> if the user is unavailable + * or if no presence information is available. + */ + public Presence getOccupantPresence(String user) { + return occupantsMap.get(user); + } + + /** + * Returns the Occupant information for a particular occupant, or <tt>null</tt> if the + * user is not in the room. The Occupant object may include information such as full + * JID of the user as well as the role and affiliation of the user in the room.<p> + * + * @param user the room occupant to search for his presence. The format of user must + * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch). + * @return the Occupant or <tt>null</tt> if the user is unavailable (i.e. not in the room). + */ + public Occupant getOccupant(String user) { + Presence presence = occupantsMap.get(user); + if (presence != null) { + return new Occupant(presence); + } + return null; + } + + /** + * Adds a packet listener that will be notified of any new Presence packets + * sent to the group chat. Using a listener is a suitable way to know when the list + * of occupants should be re-loaded due to any changes. + * + * @param listener a packet listener that will be notified of any presence packets + * sent to the group chat. + */ + public void addParticipantListener(PacketListener listener) { + connection.addPacketListener(listener, presenceFilter); + connectionListeners.add(listener); + } + + /** + * Remoces a packet listener that was being notified of any new Presence packets + * sent to the group chat. + * + * @param listener a packet listener that was being notified of any presence packets + * sent to the group chat. + */ + public void removeParticipantListener(PacketListener listener) { + connection.removePacketListener(listener); + connectionListeners.remove(listener); + } + + /** + * Returns a collection of <code>Affiliate</code> with the room owners. + * + * @return a collection of <code>Affiliate</code> with the room owners. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + public Collection<Affiliate> getOwners() throws XMPPException { + return getAffiliatesByAdmin("owner"); + } + + /** + * Returns a collection of <code>Affiliate</code> with the room administrators. + * + * @return a collection of <code>Affiliate</code> with the room administrators. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + public Collection<Affiliate> getAdmins() throws XMPPException { + return getAffiliatesByOwner("admin"); + } + + /** + * Returns a collection of <code>Affiliate</code> with the room members. + * + * @return a collection of <code>Affiliate</code> with the room members. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + public Collection<Affiliate> getMembers() throws XMPPException { + return getAffiliatesByAdmin("member"); + } + + /** + * Returns a collection of <code>Affiliate</code> with the room outcasts. + * + * @return a collection of <code>Affiliate</code> with the room outcasts. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + public Collection<Affiliate> getOutcasts() throws XMPPException { + return getAffiliatesByAdmin("outcast"); + } + + /** + * Returns a collection of <code>Affiliate</code> that have the specified room affiliation + * sending a request in the owner namespace. + * + * @param affiliation the affiliation of the users in the room. + * @return a collection of <code>Affiliate</code> that have the specified room affiliation. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + private Collection<Affiliate> getAffiliatesByOwner(String affiliation) throws XMPPException { + MUCOwner iq = new MUCOwner(); + iq.setTo(room); + iq.setType(IQ.Type.GET); + // Set the specified affiliation. This may request the list of owners/admins/members/outcasts. + MUCOwner.Item item = new MUCOwner.Item(affiliation); + iq.addItem(item); + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + MUCOwner answer = (MUCOwner) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + // Get the list of affiliates from the server's answer + List<Affiliate> affiliates = new ArrayList<Affiliate>(); + for (Iterator<MUCOwner.Item> it = answer.getItems(); it.hasNext();) { + affiliates.add(new Affiliate(it.next())); + } + return affiliates; + } + + /** + * Returns a collection of <code>Affiliate</code> that have the specified room affiliation + * sending a request in the admin namespace. + * + * @param affiliation the affiliation of the users in the room. + * @return a collection of <code>Affiliate</code> that have the specified room affiliation. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + private Collection<Affiliate> getAffiliatesByAdmin(String affiliation) throws XMPPException { + MUCAdmin iq = new MUCAdmin(); + iq.setTo(room); + iq.setType(IQ.Type.GET); + // Set the specified affiliation. This may request the list of owners/admins/members/outcasts. + MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null); + iq.addItem(item); + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + // Get the list of affiliates from the server's answer + List<Affiliate> affiliates = new ArrayList<Affiliate>(); + for (Iterator<MUCAdmin.Item> it = answer.getItems(); it.hasNext();) { + affiliates.add(new Affiliate(it.next())); + } + return affiliates; + } + + /** + * Returns a collection of <code>Occupant</code> with the room moderators. + * + * @return a collection of <code>Occupant</code> with the room moderators. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + public Collection<Occupant> getModerators() throws XMPPException { + return getOccupants("moderator"); + } + + /** + * Returns a collection of <code>Occupant</code> with the room participants. + * + * @return a collection of <code>Occupant</code> with the room participants. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + public Collection<Occupant> getParticipants() throws XMPPException { + return getOccupants("participant"); + } + + /** + * Returns a collection of <code>Occupant</code> that have the specified room role. + * + * @param role the role of the occupant in the room. + * @return a collection of <code>Occupant</code> that have the specified room role. + * @throws XMPPException if an error occured while performing the request to the server or you + * don't have enough privileges to get this information. + */ + private Collection<Occupant> getOccupants(String role) throws XMPPException { + MUCAdmin iq = new MUCAdmin(); + iq.setTo(room); + iq.setType(IQ.Type.GET); + // Set the specified role. This may request the list of moderators/participants. + MUCAdmin.Item item = new MUCAdmin.Item(null, role); + iq.addItem(item); + + // Wait for a response packet back from the server. + PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID()); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send the request to the server. + connection.sendPacket(iq); + // Wait up to a certain number of seconds for a reply. + MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + // Get the list of participants from the server's answer + List<Occupant> participants = new ArrayList<Occupant>(); + for (Iterator<MUCAdmin.Item> it = answer.getItems(); it.hasNext();) { + participants.add(new Occupant(it.next())); + } + return participants; + } + + /** + * Sends a message to the chat room. + * + * @param text the text of the message to send. + * @throws XMPPException if sending the message fails. + */ + public void sendMessage(String text) throws XMPPException { + Message message = new Message(room, Message.Type.groupchat); + message.setBody(text); + connection.sendPacket(message); + } + + /** + * Returns a new Chat for sending private messages to a given room occupant. + * The Chat's occupant address is the room's JID (i.e. roomName@service/nick). The server + * service will change the 'from' address to the sender's room JID and delivering the message + * to the intended recipient's full JID. + * + * @param occupant occupant unique room JID (e.g. 'darkcave@macbeth.shakespeare.lit/Paul'). + * @param listener the listener is a message listener that will handle messages for the newly + * created chat. + * @return new Chat for sending private messages to a given room occupant. + */ + public Chat createPrivateChat(String occupant, MessageListener listener) { + return connection.getChatManager().createChat(occupant, listener); + } + + /** + * Creates a new Message to send to the chat room. + * + * @return a new Message addressed to the chat room. + */ + public Message createMessage() { + return new Message(room, Message.Type.groupchat); + } + + /** + * Sends a Message to the chat room. + * + * @param message the message. + * @throws XMPPException if sending the message fails. + */ + public void sendMessage(Message message) throws XMPPException { + connection.sendPacket(message); + } + + /** + * Polls for and returns the next message, or <tt>null</tt> if there isn't + * a message immediately available. This method provides significantly different + * functionalty than the {@link #nextMessage()} method since it's non-blocking. + * In other words, the method call will always return immediately, whereas the + * nextMessage method will return only when a message is available (or after + * a specific timeout). + * + * @return the next message if one is immediately available and + * <tt>null</tt> otherwise. + */ + public Message pollMessage() { + return (Message) messageCollector.pollResult(); + } + + /** + * Returns the next available message in the chat. The method call will block + * (not return) until a message is available. + * + * @return the next message. + */ + public Message nextMessage() { + return (Message) messageCollector.nextResult(); + } + + /** + * Returns the next available message in the chat. The method call will block + * (not return) until a packet is available or the <tt>timeout</tt> has elapased. + * If the timeout elapses without a result, <tt>null</tt> will be returned. + * + * @param timeout the maximum amount of time to wait for the next message. + * @return the next message, or <tt>null</tt> if the timeout elapses without a + * message becoming available. + */ + public Message nextMessage(long timeout) { + return (Message) messageCollector.nextResult(timeout); + } + + /** + * Adds a packet listener that will be notified of any new messages in the + * group chat. Only "group chat" messages addressed to this group chat will + * be delivered to the listener. If you wish to listen for other packets + * that may be associated with this group chat, you should register a + * PacketListener directly with the Connection with the appropriate + * PacketListener. + * + * @param listener a packet listener. + */ + public void addMessageListener(PacketListener listener) { + connection.addPacketListener(listener, messageFilter); + connectionListeners.add(listener); + } + + /** + * Removes a packet listener that was being notified of any new messages in the + * multi user chat. Only "group chat" messages addressed to this multi user chat were + * being delivered to the listener. + * + * @param listener a packet listener. + */ + public void removeMessageListener(PacketListener listener) { + connection.removePacketListener(listener); + connectionListeners.remove(listener); + } + + /** + * Changes the subject within the room. As a default, only users with a role of "moderator" + * are allowed to change the subject in a room. Although some rooms may be configured to + * allow a mere participant or even a visitor to change the subject. + * + * @param subject the new room's subject to set. + * @throws XMPPException if someone without appropriate privileges attempts to change the + * room subject will throw an error with code 403 (i.e. Forbidden) + */ + public void changeSubject(final String subject) throws XMPPException { + Message message = new Message(room, Message.Type.groupchat); + message.setSubject(subject); + // Wait for an error or confirmation message back from the server. + PacketFilter responseFilter = + new AndFilter( + new FromMatchesFilter(room), + new PacketTypeFilter(Message.class)); + responseFilter = new AndFilter(responseFilter, new PacketFilter() { + public boolean accept(Packet packet) { + Message msg = (Message) packet; + return subject.equals(msg.getSubject()); + } + }); + PacketCollector response = connection.createPacketCollector(responseFilter); + // Send change subject packet. + connection.sendPacket(message); + // Wait up to a certain number of seconds for a reply. + Message answer = + (Message) response.nextResult(SmackConfiguration.getPacketReplyTimeout()); + // Stop queuing results + response.cancel(); + + if (answer == null) { + throw new XMPPException("No response from server."); + } + else if (answer.getError() != null) { + throw new XMPPException(answer.getError()); + } + } + + /** + * Notification message that the user has joined the room. + */ + private synchronized void userHasJoined() { + // Update the list of joined rooms through this connection + List<String> rooms = joinedRooms.get(connection); + if (rooms == null) { + rooms = new ArrayList<String>(); + joinedRooms.put(connection, rooms); + } + rooms.add(room); + } + + /** + * Notification message that the user has left the room. + */ + private synchronized void userHasLeft() { + // Update the list of joined rooms through this connection + List<String> rooms = joinedRooms.get(connection); + if (rooms == null) { + return; + } + rooms.remove(room); + cleanup(); + } + + /** + * Returns the MUCUser packet extension included in the packet or <tt>null</tt> if none. + * + * @param packet the packet that may include the MUCUser extension. + * @return the MUCUser found in the packet. + */ + private MUCUser getMUCUserExtension(Packet packet) { + if (packet != null) { + // Get the MUC User extension + return (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user"); + } + return null; + } + + /** + * Adds a listener that will be notified of changes in your status in the room + * such as the user being kicked, banned, or granted admin permissions. + * + * @param listener a user status listener. + */ + public void addUserStatusListener(UserStatusListener listener) { + synchronized (userStatusListeners) { + if (!userStatusListeners.contains(listener)) { + userStatusListeners.add(listener); + } + } + } + + /** + * Removes a listener that was being notified of changes in your status in the room + * such as the user being kicked, banned, or granted admin permissions. + * + * @param listener a user status listener. + */ + public void removeUserStatusListener(UserStatusListener listener) { + synchronized (userStatusListeners) { + userStatusListeners.remove(listener); + } + } + + private void fireUserStatusListeners(String methodName, Object[] params) { + UserStatusListener[] listeners; + synchronized (userStatusListeners) { + listeners = new UserStatusListener[userStatusListeners.size()]; + userStatusListeners.toArray(listeners); + } + // Get the classes of the method parameters + Class<?>[] paramClasses = new Class[params.length]; + for (int i = 0; i < params.length; i++) { + paramClasses[i] = params[i].getClass(); + } + try { + // Get the method to execute based on the requested methodName and parameters classes + Method method = UserStatusListener.class.getDeclaredMethod(methodName, paramClasses); + for (UserStatusListener listener : listeners) { + method.invoke(listener, params); + } + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + /** + * Adds a listener that will be notified of changes in occupants status in the room + * such as the user being kicked, banned, or granted admin permissions. + * + * @param listener a participant status listener. + */ + public void addParticipantStatusListener(ParticipantStatusListener listener) { + synchronized (participantStatusListeners) { + if (!participantStatusListeners.contains(listener)) { + participantStatusListeners.add(listener); + } + } + } + + /** + * Removes a listener that was being notified of changes in occupants status in the room + * such as the user being kicked, banned, or granted admin permissions. + * + * @param listener a participant status listener. + */ + public void removeParticipantStatusListener(ParticipantStatusListener listener) { + synchronized (participantStatusListeners) { + participantStatusListeners.remove(listener); + } + } + + private void fireParticipantStatusListeners(String methodName, List<String> params) { + ParticipantStatusListener[] listeners; + synchronized (participantStatusListeners) { + listeners = new ParticipantStatusListener[participantStatusListeners.size()]; + participantStatusListeners.toArray(listeners); + } + try { + // Get the method to execute based on the requested methodName and parameter + Class<?>[] classes = new Class[params.size()]; + for (int i=0;i<params.size(); i++) { + classes[i] = String.class; + } + Method method = ParticipantStatusListener.class.getDeclaredMethod(methodName, classes); + for (ParticipantStatusListener listener : listeners) { + method.invoke(listener, params.toArray()); + } + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + private void init() { + // Create filters + messageFilter = + new AndFilter( + new FromMatchesFilter(room), + new MessageTypeFilter(Message.Type.groupchat)); + messageFilter = new AndFilter(messageFilter, new PacketFilter() { + public boolean accept(Packet packet) { + Message msg = (Message) packet; + return msg.getBody() != null; + } + }); + presenceFilter = + new AndFilter(new FromMatchesFilter(room), new PacketTypeFilter(Presence.class)); + + // Create a collector for incoming messages. + messageCollector = new ConnectionDetachedPacketCollector(); + + // Create a listener for subject updates. + PacketListener subjectListener = new PacketListener() { + public void processPacket(Packet packet) { + Message msg = (Message) packet; + // Update the room subject + subject = msg.getSubject(); + // Fire event for subject updated listeners + fireSubjectUpdatedListeners( + msg.getSubject(), + msg.getFrom()); + + } + }; + + // Create a listener for all presence updates. + PacketListener presenceListener = new PacketListener() { + public void processPacket(Packet packet) { + Presence presence = (Presence) packet; + String from = presence.getFrom(); + String myRoomJID = room + "/" + nickname; + boolean isUserStatusModification = presence.getFrom().equals(myRoomJID); + if (presence.getType() == Presence.Type.available) { + Presence oldPresence = occupantsMap.put(from, presence); + if (oldPresence != null) { + // Get the previous occupant's affiliation & role + MUCUser mucExtension = getMUCUserExtension(oldPresence); + String oldAffiliation = mucExtension.getItem().getAffiliation(); + String oldRole = mucExtension.getItem().getRole(); + // Get the new occupant's affiliation & role + mucExtension = getMUCUserExtension(presence); + String newAffiliation = mucExtension.getItem().getAffiliation(); + String newRole = mucExtension.getItem().getRole(); + // Fire role modification events + checkRoleModifications(oldRole, newRole, isUserStatusModification, from); + // Fire affiliation modification events + checkAffiliationModifications( + oldAffiliation, + newAffiliation, + isUserStatusModification, + from); + } + else { + // A new occupant has joined the room + if (!isUserStatusModification) { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("joined", params); + } + } + } + else if (presence.getType() == Presence.Type.unavailable) { + occupantsMap.remove(from); + MUCUser mucUser = getMUCUserExtension(presence); + if (mucUser != null && mucUser.getStatus() != null) { + // Fire events according to the received presence code + checkPresenceCode( + mucUser.getStatus().getCode(), + presence.getFrom().equals(myRoomJID), + mucUser, + from); + } else { + // An occupant has left the room + if (!isUserStatusModification) { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("left", params); + } + } + } + } + }; + + // Listens for all messages that include a MUCUser extension and fire the invitation + // rejection listeners if the message includes an invitation rejection. + PacketListener declinesListener = new PacketListener() { + public void processPacket(Packet packet) { + // Get the MUC User extension + MUCUser mucUser = getMUCUserExtension(packet); + // Check if the MUCUser informs that the invitee has declined the invitation + if (mucUser.getDecline() != null && + ((Message) packet).getType() != Message.Type.error) { + // Fire event for invitation rejection listeners + fireInvitationRejectionListeners( + mucUser.getDecline().getFrom(), + mucUser.getDecline().getReason()); + } + } + }; + + PacketMultiplexListener packetMultiplexor = new PacketMultiplexListener( + messageCollector, presenceListener, subjectListener, + declinesListener); + + roomListenerMultiplexor = RoomListenerMultiplexor.getRoomMultiplexor(connection); + + roomListenerMultiplexor.addRoom(room, packetMultiplexor); + } + + /** + * Fires notification events if the role of a room occupant has changed. If the occupant that + * changed his role is your occupant then the <code>UserStatusListeners</code> added to this + * <code>MultiUserChat</code> will be fired. On the other hand, if the occupant that changed + * his role is not yours then the <code>ParticipantStatusListeners</code> added to this + * <code>MultiUserChat</code> will be fired. The following table shows the events that will + * be fired depending on the previous and new role of the occupant. + * + * <pre> + * <table border="1"> + * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr> + * + * <tr><td>None</td><td>Visitor</td><td>--</td></tr> + * <tr><td>Visitor</td><td>Participant</td><td>voiceGranted</td></tr> + * <tr><td>Participant</td><td>Moderator</td><td>moderatorGranted</td></tr> + * + * <tr><td>None</td><td>Participant</td><td>voiceGranted</td></tr> + * <tr><td>None</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr> + * <tr><td>Visitor</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr> + * + * <tr><td>Moderator</td><td>Participant</td><td>moderatorRevoked</td></tr> + * <tr><td>Participant</td><td>Visitor</td><td>voiceRevoked</td></tr> + * <tr><td>Visitor</td><td>None</td><td>kicked</td></tr> + * + * <tr><td>Moderator</td><td>Visitor</td><td>voiceRevoked + moderatorRevoked</td></tr> + * <tr><td>Moderator</td><td>None</td><td>kicked</td></tr> + * <tr><td>Participant</td><td>None</td><td>kicked</td></tr> + * </table> + * </pre> + * + * @param oldRole the previous role of the user in the room before receiving the new presence + * @param newRole the new role of the user in the room after receiving the new presence + * @param isUserModification whether the received presence is about your user in the room or not + * @param from the occupant whose role in the room has changed + * (e.g. room@conference.jabber.org/nick). + */ + private void checkRoleModifications( + String oldRole, + String newRole, + boolean isUserModification, + String from) { + // Voice was granted to a visitor + if (("visitor".equals(oldRole) || "none".equals(oldRole)) + && "participant".equals(newRole)) { + if (isUserModification) { + fireUserStatusListeners("voiceGranted", new Object[] {}); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("voiceGranted", params); + } + } + // The participant's voice was revoked from the room + else if ( + "participant".equals(oldRole) + && ("visitor".equals(newRole) || "none".equals(newRole))) { + if (isUserModification) { + fireUserStatusListeners("voiceRevoked", new Object[] {}); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("voiceRevoked", params); + } + } + // Moderator privileges were granted to a participant + if (!"moderator".equals(oldRole) && "moderator".equals(newRole)) { + if ("visitor".equals(oldRole) || "none".equals(oldRole)) { + if (isUserModification) { + fireUserStatusListeners("voiceGranted", new Object[] {}); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("voiceGranted", params); + } + } + if (isUserModification) { + fireUserStatusListeners("moderatorGranted", new Object[] {}); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("moderatorGranted", params); + } + } + // Moderator privileges were revoked from a participant + else if ("moderator".equals(oldRole) && !"moderator".equals(newRole)) { + if ("visitor".equals(newRole) || "none".equals(newRole)) { + if (isUserModification) { + fireUserStatusListeners("voiceRevoked", new Object[] {}); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("voiceRevoked", params); + } + } + if (isUserModification) { + fireUserStatusListeners("moderatorRevoked", new Object[] {}); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("moderatorRevoked", params); + } + } + } + + /** + * Fires notification events if the affiliation of a room occupant has changed. If the + * occupant that changed his affiliation is your occupant then the + * <code>UserStatusListeners</code> added to this <code>MultiUserChat</code> will be fired. + * On the other hand, if the occupant that changed his affiliation is not yours then the + * <code>ParticipantStatusListeners</code> added to this <code>MultiUserChat</code> will be + * fired. The following table shows the events that will be fired depending on the previous + * and new affiliation of the occupant. + * + * <pre> + * <table border="1"> + * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr> + * + * <tr><td>None</td><td>Member</td><td>membershipGranted</td></tr> + * <tr><td>Member</td><td>Admin</td><td>membershipRevoked + adminGranted</td></tr> + * <tr><td>Admin</td><td>Owner</td><td>adminRevoked + ownershipGranted</td></tr> + * + * <tr><td>None</td><td>Admin</td><td>adminGranted</td></tr> + * <tr><td>None</td><td>Owner</td><td>ownershipGranted</td></tr> + * <tr><td>Member</td><td>Owner</td><td>membershipRevoked + ownershipGranted</td></tr> + * + * <tr><td>Owner</td><td>Admin</td><td>ownershipRevoked + adminGranted</td></tr> + * <tr><td>Admin</td><td>Member</td><td>adminRevoked + membershipGranted</td></tr> + * <tr><td>Member</td><td>None</td><td>membershipRevoked</td></tr> + * + * <tr><td>Owner</td><td>Member</td><td>ownershipRevoked + membershipGranted</td></tr> + * <tr><td>Owner</td><td>None</td><td>ownershipRevoked</td></tr> + * <tr><td>Admin</td><td>None</td><td>adminRevoked</td></tr> + * <tr><td><i>Anyone</i></td><td>Outcast</td><td>banned</td></tr> + * </table> + * </pre> + * + * @param oldAffiliation the previous affiliation of the user in the room before receiving the + * new presence + * @param newAffiliation the new affiliation of the user in the room after receiving the new + * presence + * @param isUserModification whether the received presence is about your user in the room or not + * @param from the occupant whose role in the room has changed + * (e.g. room@conference.jabber.org/nick). + */ + private void checkAffiliationModifications( + String oldAffiliation, + String newAffiliation, + boolean isUserModification, + String from) { + // First check for revoked affiliation and then for granted affiliations. The idea is to + // first fire the "revoke" events and then fire the "grant" events. + + // The user's ownership to the room was revoked + if ("owner".equals(oldAffiliation) && !"owner".equals(newAffiliation)) { + if (isUserModification) { + fireUserStatusListeners("ownershipRevoked", new Object[] {}); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("ownershipRevoked", params); + } + } + // The user's administrative privileges to the room were revoked + else if ("admin".equals(oldAffiliation) && !"admin".equals(newAffiliation)) { + if (isUserModification) { + fireUserStatusListeners("adminRevoked", new Object[] {}); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("adminRevoked", params); + } + } + // The user's membership to the room was revoked + else if ("member".equals(oldAffiliation) && !"member".equals(newAffiliation)) { + if (isUserModification) { + fireUserStatusListeners("membershipRevoked", new Object[] {}); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("membershipRevoked", params); + } + } + + // The user was granted ownership to the room + if (!"owner".equals(oldAffiliation) && "owner".equals(newAffiliation)) { + if (isUserModification) { + fireUserStatusListeners("ownershipGranted", new Object[] {}); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("ownershipGranted", params); + } + } + // The user was granted administrative privileges to the room + else if (!"admin".equals(oldAffiliation) && "admin".equals(newAffiliation)) { + if (isUserModification) { + fireUserStatusListeners("adminGranted", new Object[] {}); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("adminGranted", params); + } + } + // The user was granted membership to the room + else if (!"member".equals(oldAffiliation) && "member".equals(newAffiliation)) { + if (isUserModification) { + fireUserStatusListeners("membershipGranted", new Object[] {}); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + fireParticipantStatusListeners("membershipGranted", params); + } + } + } + + /** + * Fires events according to the received presence code. + * + * @param code + * @param isUserModification + * @param mucUser + * @param from + */ + private void checkPresenceCode( + String code, + boolean isUserModification, + MUCUser mucUser, + String from) { + // Check if an occupant was kicked from the room + if ("307".equals(code)) { + // Check if this occupant was kicked + if (isUserModification) { + joined = false; + + fireUserStatusListeners( + "kicked", + new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()}); + + // Reset occupant information. + occupantsMap.clear(); + nickname = null; + userHasLeft(); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + params.add(mucUser.getItem().getActor()); + params.add(mucUser.getItem().getReason()); + fireParticipantStatusListeners("kicked", params); + } + } + // A user was banned from the room + else if ("301".equals(code)) { + // Check if this occupant was banned + if (isUserModification) { + joined = false; + + fireUserStatusListeners( + "banned", + new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()}); + + // Reset occupant information. + occupantsMap.clear(); + nickname = null; + userHasLeft(); + } + else { + List<String> params = new ArrayList<String>(); + params.add(from); + params.add(mucUser.getItem().getActor()); + params.add(mucUser.getItem().getReason()); + fireParticipantStatusListeners("banned", params); + } + } + // A user's membership was revoked from the room + else if ("321".equals(code)) { + // Check if this occupant's membership was revoked + if (isUserModification) { + joined = false; + + fireUserStatusListeners("membershipRevoked", new Object[] {}); + + // Reset occupant information. + occupantsMap.clear(); + nickname = null; + userHasLeft(); + } + } + // A occupant has changed his nickname in the room + else if ("303".equals(code)) { + List<String> params = new ArrayList<String>(); + params.add(from); + params.add(mucUser.getItem().getNick()); + fireParticipantStatusListeners("nicknameChanged", params); + } + } + + private void cleanup() { + try { + if (connection != null) { + roomListenerMultiplexor.removeRoom(room); + // Remove all the PacketListeners added to the connection by this chat + for (PacketListener connectionListener : connectionListeners) { + connection.removePacketListener(connectionListener); + } + } + } catch (Exception e) { + // Do nothing + } + } + + protected void finalize() throws Throwable { + cleanup(); + super.finalize(); + } + + /** + * An InvitationsMonitor monitors a given connection to detect room invitations. Every + * time the InvitationsMonitor detects a new invitation it will fire the invitation listeners. + * + * @author Gaston Dombiak + */ + private static class InvitationsMonitor implements ConnectionListener { + // We use a WeakHashMap so that the GC can collect the monitor when the + // connection is no longer referenced by any object. + // Note that when the InvitationsMonitor is used, i.e. when there are InvitationListeners, it will add a + // PacketListener to the Connection and therefore a strong reference from the Connection to the + // InvitationsMonior will exists, preventing it from beeing gc'ed. After the last InvitationListener is gone, + // the PacketListener will get removed (cancel()) allowing the garbage collection of the InvitationsMonitor + // instance. + private final static Map<Connection, WeakReference<InvitationsMonitor>> monitors = + new WeakHashMap<Connection, WeakReference<InvitationsMonitor>>(); + + // We don't use a synchronized List here because it would break the semantic of (add|remove)InvitationListener + private final List<InvitationListener> invitationsListeners = + new ArrayList<InvitationListener>(); + private Connection connection; + private PacketFilter invitationFilter; + private PacketListener invitationPacketListener; + + /** + * Returns a new or existing InvitationsMonitor for a given connection. + * + * @param conn the connection to monitor for room invitations. + * @return a new or existing InvitationsMonitor for a given connection. + */ + public static InvitationsMonitor getInvitationsMonitor(Connection conn) { + synchronized (monitors) { + if (!monitors.containsKey(conn) || monitors.get(conn).get() == null) { + // We need to use a WeakReference because the monitor references the + // connection and this could prevent the GC from collecting the monitor + // when no other object references the monitor + InvitationsMonitor ivm = new InvitationsMonitor(conn); + monitors.put(conn, new WeakReference<InvitationsMonitor>(ivm)); + return ivm; + } + // Return the InvitationsMonitor that monitors the connection + return monitors.get(conn).get(); + } + } + + /** + * Creates a new InvitationsMonitor that will monitor invitations received + * on a given connection. + * + * @param connection the connection to monitor for possible room invitations + */ + private InvitationsMonitor(Connection connection) { + this.connection = connection; + } + + /** + * Adds a listener to invitation notifications. The listener will be fired anytime + * an invitation is received.<p> + * + * If this is the first monitor's listener then the monitor will be initialized in + * order to start listening to room invitations. + * + * @param listener an invitation listener. + */ + public void addInvitationListener(InvitationListener listener) { + synchronized (invitationsListeners) { + // If this is the first monitor's listener then initialize the listeners + // on the connection to detect room invitations + if (invitationsListeners.size() == 0) { + init(); + } + if (!invitationsListeners.contains(listener)) { + invitationsListeners.add(listener); + } + } + } + + /** + * Removes a listener to invitation notifications. The listener will be fired anytime + * an invitation is received.<p> + * + * If there are no more listeners to notifiy for room invitations then the monitor will + * be stopped. As soon as a new listener is added to the monitor, the monitor will resume + * monitoring the connection for new room invitations. + * + * @param listener an invitation listener. + */ + public void removeInvitationListener(InvitationListener listener) { + synchronized (invitationsListeners) { + if (invitationsListeners.contains(listener)) { + invitationsListeners.remove(listener); + } + // If there are no more listeners to notifiy for room invitations + // then proceed to cancel/release this monitor + if (invitationsListeners.size() == 0) { + cancel(); + } + } + } + + /** + * Fires invitation listeners. + */ + private void fireInvitationListeners(String room, String inviter, String reason, String password, + Message message) { + InvitationListener[] listeners; + synchronized (invitationsListeners) { + listeners = new InvitationListener[invitationsListeners.size()]; + invitationsListeners.toArray(listeners); + } + for (InvitationListener listener : listeners) { + listener.invitationReceived(connection, room, inviter, reason, password, message); + } + } + + public void connectionClosed() { + cancel(); + } + + public void connectionClosedOnError(Exception e) { + // ignore + } + + public void reconnectingIn(int seconds) { + // ignore + } + + public void reconnectionSuccessful() { + // ignore + } + + public void reconnectionFailed(Exception e) { + // ignore + } + + /** + * Initializes the listeners to detect received room invitations and to detect when the + * connection gets closed. As soon as a room invitation is received the invitations + * listeners will be fired. When the connection gets closed the monitor will remove + * his listeners on the connection. + */ + private void init() { + // Listens for all messages that include a MUCUser extension and fire the invitation + // listeners if the message includes an invitation. + invitationFilter = + new PacketExtensionFilter("x", "http://jabber.org/protocol/muc#user"); + invitationPacketListener = new PacketListener() { + public void processPacket(Packet packet) { + // Get the MUCUser extension + MUCUser mucUser = + (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user"); + // Check if the MUCUser extension includes an invitation + if (mucUser.getInvite() != null && + ((Message) packet).getType() != Message.Type.error) { + // Fire event for invitation listeners + fireInvitationListeners(packet.getFrom(), mucUser.getInvite().getFrom(), + mucUser.getInvite().getReason(), mucUser.getPassword(), (Message) packet); + } + } + }; + connection.addPacketListener(invitationPacketListener, invitationFilter); + // Add a listener to detect when the connection gets closed in order to + // cancel/release this monitor + connection.addConnectionListener(this); + } + + /** + * Cancels all the listeners that this InvitationsMonitor has added to the connection. + */ + private void cancel() { + connection.removePacketListener(invitationPacketListener); + connection.removeConnectionListener(this); + } + + } +} diff --git a/src/org/jivesoftware/smackx/muc/Occupant.java b/src/org/jivesoftware/smackx/muc/Occupant.java new file mode 100644 index 0000000..3b199a5 --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/Occupant.java @@ -0,0 +1,121 @@ +/** + * $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.smackx.muc; + +import org.jivesoftware.smackx.packet.MUCAdmin; +import org.jivesoftware.smackx.packet.MUCUser; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.util.StringUtils; + +/** + * Represents the information about an occupant in a given room. The information will always have + * the affiliation and role of the occupant in the room. The full JID and nickname are optional. + * + * @author Gaston Dombiak + */ +public class Occupant { + // Fields that must have a value + private String affiliation; + private String role; + // Fields that may have a value + private String jid; + private String nick; + + Occupant(MUCAdmin.Item item) { + super(); + this.jid = item.getJid(); + this.affiliation = item.getAffiliation(); + this.role = item.getRole(); + this.nick = item.getNick(); + } + + Occupant(Presence presence) { + super(); + MUCUser mucUser = (MUCUser) presence.getExtension("x", + "http://jabber.org/protocol/muc#user"); + MUCUser.Item item = mucUser.getItem(); + this.jid = item.getJid(); + this.affiliation = item.getAffiliation(); + this.role = item.getRole(); + // Get the nickname from the FROM attribute of the presence + this.nick = StringUtils.parseResource(presence.getFrom()); + } + + /** + * Returns the full JID of the occupant. If this information was extracted from a presence and + * the room is semi or full-anonymous then the answer will be null. On the other hand, if this + * information was obtained while maintaining the voice list or the moderator list then we will + * always have a full JID. + * + * @return the full JID of the occupant. + */ + public String getJid() { + return jid; + } + + /** + * Returns the affiliation of the occupant. Possible affiliations are: "owner", "admin", + * "member", "outcast". This information will always be available. + * + * @return the affiliation of the occupant. + */ + public String getAffiliation() { + return affiliation; + } + + /** + * Returns the current role of the occupant in the room. This information will always be + * available. + * + * @return the current role of the occupant in the room. + */ + public String getRole() { + return role; + } + + /** + * Returns the current nickname of the occupant in the room. If this information was extracted + * from a presence then the answer will be null. + * + * @return the current nickname of the occupant in the room or null if this information was + * obtained from a presence. + */ + public String getNick() { + return nick; + } + + public boolean equals(Object obj) { + if(!(obj instanceof Occupant)) { + return false; + } + Occupant occupant = (Occupant)obj; + return jid.equals(occupant.jid); + } + + public int hashCode() { + int result; + result = affiliation.hashCode(); + result = 17 * result + role.hashCode(); + result = 17 * result + jid.hashCode(); + result = 17 * result + (nick != null ? nick.hashCode() : 0); + return result; + } +} diff --git a/src/org/jivesoftware/smackx/muc/PacketMultiplexListener.java b/src/org/jivesoftware/smackx/muc/PacketMultiplexListener.java new file mode 100644 index 0000000..c1863c2 --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/PacketMultiplexListener.java @@ -0,0 +1,96 @@ +/** + * $RCSfile$ + * $Revision: 2779 $ + * $Date: 2005-09-05 17:00:45 -0300 (Mon, 05 Sep 2005) $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.muc; + +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.filter.MessageTypeFilter; +import org.jivesoftware.smack.filter.PacketExtensionFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.Presence; + +/** + * The single <code>PacketListener</code> used by each {@link MultiUserChat} + * for all basic processing of presence, and message packets targeted to that chat. + * + * @author Larry Kirschner + */ +class PacketMultiplexListener implements PacketListener { + + private static final PacketFilter MESSAGE_FILTER = + new MessageTypeFilter(Message.Type.groupchat); + private static final PacketFilter PRESENCE_FILTER = new PacketTypeFilter(Presence.class); + private static final PacketFilter SUBJECT_FILTER = new PacketFilter() { + public boolean accept(Packet packet) { + Message msg = (Message) packet; + return msg.getSubject() != null; + } + }; + private static final PacketFilter DECLINES_FILTER = + new PacketExtensionFilter("x", + "http://jabber.org/protocol/muc#user"); + + private ConnectionDetachedPacketCollector messageCollector; + private PacketListener presenceListener; + private PacketListener subjectListener; + private PacketListener declinesListener; + + public PacketMultiplexListener( + ConnectionDetachedPacketCollector messageCollector, + PacketListener presenceListener, + PacketListener subjectListener, PacketListener declinesListener) { + if (messageCollector == null) { + throw new IllegalArgumentException("MessageCollector is null"); + } + if (presenceListener == null) { + throw new IllegalArgumentException("Presence listener is null"); + } + if (subjectListener == null) { + throw new IllegalArgumentException("Subject listener is null"); + } + if (declinesListener == null) { + throw new IllegalArgumentException("Declines listener is null"); + } + this.messageCollector = messageCollector; + this.presenceListener = presenceListener; + this.subjectListener = subjectListener; + this.declinesListener = declinesListener; + } + + public void processPacket(Packet p) { + if (PRESENCE_FILTER.accept(p)) { + presenceListener.processPacket(p); + } + else if (MESSAGE_FILTER.accept(p)) { + messageCollector.processPacket(p); + + if (SUBJECT_FILTER.accept(p)) { + subjectListener.processPacket(p); + } + } + else if (DECLINES_FILTER.accept(p)) { + declinesListener.processPacket(p); + } + } + +} diff --git a/src/org/jivesoftware/smackx/muc/ParticipantStatusListener.java b/src/org/jivesoftware/smackx/muc/ParticipantStatusListener.java new file mode 100644 index 0000000..c3e248d --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/ParticipantStatusListener.java @@ -0,0 +1,179 @@ +/** + * $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.smackx.muc; + +/** + * A listener that is fired anytime a participant's status in a room is changed, such as the + * user being kicked, banned, or granted admin permissions. + * + * @author Gaston Dombiak + */ +public interface ParticipantStatusListener { + + /** + * Called when a new room occupant has joined the room. Note: Take in consideration that when + * you join a room you will receive the list of current occupants in the room. This message will + * be sent for each occupant. + * + * @param participant the participant that has just joined the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void joined(String participant); + + /** + * Called when a room occupant has left the room on its own. This means that the occupant was + * neither kicked nor banned from the room. + * + * @param participant the participant that has left the room on its own. + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void left(String participant); + + /** + * Called when a room participant has been kicked from the room. This means that the kicked + * participant is no longer participating in the room. + * + * @param participant the participant that was kicked from the room + * (e.g. room@conference.jabber.org/nick). + * @param actor the moderator that kicked the occupant from the room (e.g. user@host.org). + * @param reason the reason provided by the actor to kick the occupant from the room. + */ + public abstract void kicked(String participant, String actor, String reason); + + /** + * Called when a moderator grants voice to a visitor. This means that the visitor + * can now participate in the moderated room sending messages to all occupants. + * + * @param participant the participant that was granted voice in the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void voiceGranted(String participant); + + /** + * Called when a moderator revokes voice from a participant. This means that the participant + * in the room was able to speak and now is a visitor that can't send messages to the room + * occupants. + * + * @param participant the participant that was revoked voice from the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void voiceRevoked(String participant); + + /** + * Called when an administrator or owner banned a participant from the room. This means that + * banned participant will no longer be able to join the room unless the ban has been removed. + * + * @param participant the participant that was banned from the room + * (e.g. room@conference.jabber.org/nick). + * @param actor the administrator that banned the occupant (e.g. user@host.org). + * @param reason the reason provided by the administrator to ban the occupant. + */ + public abstract void banned(String participant, String actor, String reason); + + /** + * Called when an administrator grants a user membership to the room. This means that the user + * will be able to join the members-only room. + * + * @param participant the participant that was granted membership in the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void membershipGranted(String participant); + + /** + * Called when an administrator revokes a user membership to the room. This means that the + * user will not be able to join the members-only room. + * + * @param participant the participant that was revoked membership from the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void membershipRevoked(String participant); + + /** + * Called when an administrator grants moderator privileges to a user. This means that the user + * will be able to kick users, grant and revoke voice, invite other users, modify room's + * subject plus all the partcipants privileges. + * + * @param participant the participant that was granted moderator privileges in the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void moderatorGranted(String participant); + + /** + * Called when an administrator revokes moderator privileges from a user. This means that the + * user will no longer be able to kick users, grant and revoke voice, invite other users, + * modify room's subject plus all the partcipants privileges. + * + * @param participant the participant that was revoked moderator privileges in the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void moderatorRevoked(String participant); + + /** + * Called when an owner grants a user ownership on the room. This means that the user + * will be able to change defining room features as well as perform all administrative + * functions. + * + * @param participant the participant that was granted ownership on the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void ownershipGranted(String participant); + + /** + * Called when an owner revokes a user ownership on the room. This means that the user + * will no longer be able to change defining room features as well as perform all + * administrative functions. + * + * @param participant the participant that was revoked ownership on the room + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void ownershipRevoked(String participant); + + /** + * Called when an owner grants administrator privileges to a user. This means that the user + * will be able to perform administrative functions such as banning users and edit moderator + * list. + * + * @param participant the participant that was granted administrator privileges + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void adminGranted(String participant); + + /** + * Called when an owner revokes administrator privileges from a user. This means that the user + * will no longer be able to perform administrative functions such as banning users and edit + * moderator list. + * + * @param participant the participant that was revoked administrator privileges + * (e.g. room@conference.jabber.org/nick). + */ + public abstract void adminRevoked(String participant); + + /** + * Called when a participant changed his/her nickname in the room. The new participant's + * nickname will be informed with the next available presence. + * + * @param participant the participant that was revoked administrator privileges + * (e.g. room@conference.jabber.org/nick). + * @param newNickname the new nickname that the participant decided to use. + */ + public abstract void nicknameChanged(String participant, String newNickname); + +} diff --git a/src/org/jivesoftware/smackx/muc/RoomInfo.java b/src/org/jivesoftware/smackx/muc/RoomInfo.java new file mode 100644 index 0000000..f97f544 --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/RoomInfo.java @@ -0,0 +1,190 @@ +/** + * $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.smackx.muc; + +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.packet.DiscoverInfo; + +import java.util.Iterator; + +/** + * Represents the room information that was discovered using Service Discovery. It's possible to + * obtain information about a room before joining the room but only for rooms that are public (i.e. + * rooms that may be discovered). + * + * @author Gaston Dombiak + */ +public class RoomInfo { + + /** + * JID of the room. The node of the JID is commonly used as the ID of the room or name. + */ + private String room; + /** + * Description of the room. + */ + private String description = ""; + /** + * Last known subject of the room. + */ + private String subject = ""; + /** + * Current number of occupants in the room. + */ + private int occupantsCount = -1; + /** + * A room is considered members-only if an invitation is required in order to enter the room. + * Any user that is not a member of the room won't be able to join the room unless the user + * decides to register with the room (thus becoming a member). + */ + private boolean membersOnly; + /** + * Moderated rooms enable only participants to speak. Users that join the room and aren't + * participants can't speak (they are just visitors). + */ + private boolean moderated; + /** + * Every presence packet can include the JID of every occupant unless the owner deactives this + * configuration. + */ + private boolean nonanonymous; + /** + * Indicates if users must supply a password to join the room. + */ + private boolean passwordProtected; + /** + * Persistent rooms are saved to the database to make sure that rooms configurations can be + * restored in case the server goes down. + */ + private boolean persistent; + + RoomInfo(DiscoverInfo info) { + super(); + this.room = info.getFrom(); + // Get the information based on the discovered features + this.membersOnly = info.containsFeature("muc_membersonly"); + this.moderated = info.containsFeature("muc_moderated"); + this.nonanonymous = info.containsFeature("muc_nonanonymous"); + this.passwordProtected = info.containsFeature("muc_passwordprotected"); + this.persistent = info.containsFeature("muc_persistent"); + // Get the information based on the discovered extended information + Form form = Form.getFormFrom(info); + if (form != null) { + FormField descField = form.getField("muc#roominfo_description"); + this.description = ( descField == null || !(descField.getValues().hasNext()) )? "" : descField.getValues().next(); + + FormField subjField = form.getField("muc#roominfo_subject"); + this.subject = ( subjField == null || !(subjField.getValues().hasNext()) ) ? "" : subjField.getValues().next(); + + FormField occCountField = form.getField("muc#roominfo_occupants"); + this.occupantsCount = occCountField == null ? -1 : Integer.parseInt(occCountField.getValues() + .next()); + } + } + + /** + * Returns the JID of the room whose information was discovered. + * + * @return the JID of the room whose information was discovered. + */ + public String getRoom() { + return room; + } + + /** + * Returns the discovered description of the room. + * + * @return the discovered description of the room. + */ + public String getDescription() { + return description; + } + + /** + * Returns the discovered subject of the room. The subject may be empty if the room does not + * have a subject. + * + * @return the discovered subject of the room. + */ + public String getSubject() { + return subject; + } + + /** + * Returns the discovered number of occupants that are currently in the room. If this + * information was not discovered (i.e. the server didn't send it) then a value of -1 will be + * returned. + * + * @return the number of occupants that are currently in the room or -1 if that information was + * not provided by the server. + */ + public int getOccupantsCount() { + return occupantsCount; + } + + /** + * Returns true if the room has restricted the access so that only members may enter the room. + * + * @return true if the room has restricted the access so that only members may enter the room. + */ + public boolean isMembersOnly() { + return membersOnly; + } + + /** + * Returns true if the room enabled only participants to speak. Occupants with a role of + * visitor won't be able to speak in the room. + * + * @return true if the room enabled only participants to speak. + */ + public boolean isModerated() { + return moderated; + } + + /** + * Returns true if presence packets will include the JID of every occupant. + * + * @return true if presence packets will include the JID of every occupant. + */ + public boolean isNonanonymous() { + return nonanonymous; + } + + /** + * Returns true if users musy provide a valid password in order to join the room. + * + * @return true if users musy provide a valid password in order to join the room. + */ + public boolean isPasswordProtected() { + return passwordProtected; + } + + /** + * Returns true if the room will persist after the last occupant have left the room. + * + * @return true if the room will persist after the last occupant have left the room. + */ + public boolean isPersistent() { + return persistent; + } + +} diff --git a/src/org/jivesoftware/smackx/muc/RoomListenerMultiplexor.java b/src/org/jivesoftware/smackx/muc/RoomListenerMultiplexor.java new file mode 100644 index 0000000..6f8bf05 --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/RoomListenerMultiplexor.java @@ -0,0 +1,227 @@ +/** + * $RCSfile$ + * $Revision: 2779 $ + * $Date: 2005-09-05 17:00:45 -0300 (Mon, 05 Sep 2005) $ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.muc; + +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.util.StringUtils; + +import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A <code>RoomListenerMultiplexor</code> multiplexes incoming packets on + * a <code>Connection</code> using a single listener/filter pair. + * A single <code>RoomListenerMultiplexor</code> is created for each + * {@link org.jivesoftware.smack.Connection} that has joined MUC rooms + * within its session. + * + * @author Larry Kirschner + */ +class RoomListenerMultiplexor implements ConnectionListener { + + // We use a WeakHashMap so that the GC can collect the monitor when the + // connection is no longer referenced by any object. + private static final Map<Connection, WeakReference<RoomListenerMultiplexor>> monitors = + new WeakHashMap<Connection, WeakReference<RoomListenerMultiplexor>>(); + + private Connection connection; + private RoomMultiplexFilter filter; + private RoomMultiplexListener listener; + + /** + * Returns a new or existing RoomListenerMultiplexor for a given connection. + * + * @param conn the connection to monitor for room invitations. + * @return a new or existing RoomListenerMultiplexor for a given connection. + */ + public static RoomListenerMultiplexor getRoomMultiplexor(Connection conn) { + synchronized (monitors) { + if (!monitors.containsKey(conn) || monitors.get(conn).get() == null) { + RoomListenerMultiplexor rm = new RoomListenerMultiplexor(conn, new RoomMultiplexFilter(), + new RoomMultiplexListener()); + + rm.init(); + + // We need to use a WeakReference because the monitor references the + // connection and this could prevent the GC from collecting the monitor + // when no other object references the monitor + monitors.put(conn, new WeakReference<RoomListenerMultiplexor>(rm)); + } + // Return the InvitationsMonitor that monitors the connection + return monitors.get(conn).get(); + } + } + + /** + * All access should be through + * the static method {@link #getRoomMultiplexor(Connection)}. + */ + private RoomListenerMultiplexor(Connection connection, RoomMultiplexFilter filter, + RoomMultiplexListener listener) { + if (connection == null) { + throw new IllegalArgumentException("Connection is null"); + } + if (filter == null) { + throw new IllegalArgumentException("Filter is null"); + } + if (listener == null) { + throw new IllegalArgumentException("Listener is null"); + } + this.connection = connection; + this.filter = filter; + this.listener = listener; + } + + public void addRoom(String address, PacketMultiplexListener roomListener) { + filter.addRoom(address); + listener.addRoom(address, roomListener); + } + + public void connectionClosed() { + cancel(); + } + + public void connectionClosedOnError(Exception e) { + cancel(); + } + + public void reconnectingIn(int seconds) { + // ignore + } + + public void reconnectionSuccessful() { + // ignore + } + + public void reconnectionFailed(Exception e) { + // ignore + } + + /** + * Initializes the listeners to detect received room invitations and to detect when the + * connection gets closed. As soon as a room invitation is received the invitations + * listeners will be fired. When the connection gets closed the monitor will remove + * his listeners on the connection. + */ + public void init() { + connection.addConnectionListener(this); + connection.addPacketListener(listener, filter); + } + + public void removeRoom(String address) { + filter.removeRoom(address); + listener.removeRoom(address); + } + + /** + * Cancels all the listeners that this InvitationsMonitor has added to the connection. + */ + private void cancel() { + connection.removeConnectionListener(this); + connection.removePacketListener(listener); + } + + /** + * The single <code>Connection</code>-level <code>PacketFilter</code> used by a {@link RoomListenerMultiplexor} + * for all muc chat rooms on an <code>Connection</code>. + * Each time a muc chat room is added to/removed from an + * <code>Connection</code> the address for that chat room + * is added to/removed from that <code>Connection</code>'s + * <code>RoomMultiplexFilter</code>. + */ + private static class RoomMultiplexFilter implements PacketFilter { + + private Map<String, String> roomAddressTable = new ConcurrentHashMap<String, String>(); + + public boolean accept(Packet p) { + String from = p.getFrom(); + if (from == null) { + return false; + } + return roomAddressTable.containsKey(StringUtils.parseBareAddress(from).toLowerCase()); + } + + public void addRoom(String address) { + if (address == null) { + return; + } + roomAddressTable.put(address.toLowerCase(), address); + } + + public void removeRoom(String address) { + if (address == null) { + return; + } + roomAddressTable.remove(address.toLowerCase()); + } + } + + /** + * The single <code>Connection</code>-level <code>PacketListener</code> + * used by a {@link RoomListenerMultiplexor} + * for all muc chat rooms on an <code>Connection</code>. + * Each time a muc chat room is added to/removed from an + * <code>Connection</code> the address and listener for that chat room + * are added to/removed from that <code>Connection</code>'s + * <code>RoomMultiplexListener</code>. + * + * @author Larry Kirschner + */ + private static class RoomMultiplexListener implements PacketListener { + + private Map<String, PacketMultiplexListener> roomListenersByAddress = + new ConcurrentHashMap<String, PacketMultiplexListener>(); + + public void processPacket(Packet p) { + String from = p.getFrom(); + if (from == null) { + return; + } + + PacketMultiplexListener listener = + roomListenersByAddress.get(StringUtils.parseBareAddress(from).toLowerCase()); + + if (listener != null) { + listener.processPacket(p); + } + } + + public void addRoom(String address, PacketMultiplexListener listener) { + if (address == null) { + return; + } + roomListenersByAddress.put(address.toLowerCase(), listener); + } + + public void removeRoom(String address) { + if (address == null) { + return; + } + roomListenersByAddress.remove(address.toLowerCase()); + } + } +} diff --git a/src/org/jivesoftware/smackx/muc/SubjectUpdatedListener.java b/src/org/jivesoftware/smackx/muc/SubjectUpdatedListener.java new file mode 100644 index 0000000..3a2deab --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/SubjectUpdatedListener.java @@ -0,0 +1,38 @@ +/** + * $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.smackx.muc; + +/** + * A listener that is fired anytime a MUC room changes its subject. + * + * @author Gaston Dombiak + */ +public interface SubjectUpdatedListener { + + /** + * Called when a MUC room has changed its subject. + * + * @param subject the new room's subject. + * @param from the user that changed the room's subject (e.g. room@conference.jabber.org/nick). + */ + public abstract void subjectUpdated(String subject, String from); + +} diff --git a/src/org/jivesoftware/smackx/muc/UserStatusListener.java b/src/org/jivesoftware/smackx/muc/UserStatusListener.java new file mode 100644 index 0000000..27f0f58 --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/UserStatusListener.java @@ -0,0 +1,127 @@ +/** + * $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.smackx.muc; + +/** + * A listener that is fired anytime your participant's status in a room is changed, such as the + * user being kicked, banned, or granted admin permissions. + * + * @author Gaston Dombiak + */ +public interface UserStatusListener { + + /** + * Called when a moderator kicked your user from the room. This means that you are no longer + * participanting in the room. + * + * @param actor the moderator that kicked your user from the room (e.g. user@host.org). + * @param reason the reason provided by the actor to kick you from the room. + */ + public abstract void kicked(String actor, String reason); + + /** + * Called when a moderator grants voice to your user. This means that you were a visitor in + * the moderated room before and now you can participate in the room by sending messages to + * all occupants. + * + */ + public abstract void voiceGranted(); + + /** + * Called when a moderator revokes voice from your user. This means that you were a + * participant in the room able to speak and now you are a visitor that can't send + * messages to the room occupants. + * + */ + public abstract void voiceRevoked(); + + /** + * Called when an administrator or owner banned your user from the room. This means that you + * will no longer be able to join the room unless the ban has been removed. + * + * @param actor the administrator that banned your user (e.g. user@host.org). + * @param reason the reason provided by the administrator to banned you. + */ + public abstract void banned(String actor, String reason); + + /** + * Called when an administrator grants your user membership to the room. This means that you + * will be able to join the members-only room. + * + */ + public abstract void membershipGranted(); + + /** + * Called when an administrator revokes your user membership to the room. This means that you + * will not be able to join the members-only room. + * + */ + public abstract void membershipRevoked(); + + /** + * Called when an administrator grants moderator privileges to your user. This means that you + * will be able to kick users, grant and revoke voice, invite other users, modify room's + * subject plus all the partcipants privileges. + * + */ + public abstract void moderatorGranted(); + + /** + * Called when an administrator revokes moderator privileges from your user. This means that + * you will no longer be able to kick users, grant and revoke voice, invite other users, + * modify room's subject plus all the partcipants privileges. + * + */ + public abstract void moderatorRevoked(); + + /** + * Called when an owner grants to your user ownership on the room. This means that you + * will be able to change defining room features as well as perform all administrative + * functions. + * + */ + public abstract void ownershipGranted(); + + /** + * Called when an owner revokes from your user ownership on the room. This means that you + * will no longer be able to change defining room features as well as perform all + * administrative functions. + * + */ + public abstract void ownershipRevoked(); + + /** + * Called when an owner grants administrator privileges to your user. This means that you + * will be able to perform administrative functions such as banning users and edit moderator + * list. + * + */ + public abstract void adminGranted(); + + /** + * Called when an owner revokes administrator privileges from your user. This means that you + * will no longer be able to perform administrative functions such as banning users and edit + * moderator list. + * + */ + public abstract void adminRevoked(); + +} diff --git a/src/org/jivesoftware/smackx/muc/package.html b/src/org/jivesoftware/smackx/muc/package.html new file mode 100644 index 0000000..dcfaeaa --- /dev/null +++ b/src/org/jivesoftware/smackx/muc/package.html @@ -0,0 +1 @@ +<body>Classes and Interfaces that implement Multi-User Chat (MUC).</body>
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/package.html b/src/org/jivesoftware/smackx/package.html new file mode 100644 index 0000000..d574a2a --- /dev/null +++ b/src/org/jivesoftware/smackx/package.html @@ -0,0 +1 @@ +<body>Smack extensions API.</body>
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/packet/AdHocCommandData.java b/src/org/jivesoftware/smackx/packet/AdHocCommandData.java new file mode 100755 index 0000000..bceffcd --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/AdHocCommandData.java @@ -0,0 +1,279 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2005-2008 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jivesoftware.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.commands.AdHocCommand;
+import org.jivesoftware.smackx.commands.AdHocCommand.Action;
+import org.jivesoftware.smackx.commands.AdHocCommand.SpecificErrorCondition;
+import org.jivesoftware.smackx.commands.AdHocCommandNote;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents the state and the request of the execution of an adhoc command.
+ *
+ * @author Gabriel Guardincerri
+ */
+public class AdHocCommandData extends IQ {
+
+ /* JID of the command host */
+ private String id;
+
+ /* Command name */
+ private String name;
+
+ /* Command identifier */
+ private String node;
+
+ /* Unique ID of the execution */
+ private String sessionID;
+
+ private List<AdHocCommandNote> notes = new ArrayList<AdHocCommandNote>();
+
+ private DataForm form;
+
+ /* Action request to be executed */
+ private AdHocCommand.Action action;
+
+ /* Current execution status */
+ private AdHocCommand.Status status;
+
+ private ArrayList<AdHocCommand.Action> actions = new ArrayList<AdHocCommand.Action>();
+
+ private AdHocCommand.Action executeAction;
+
+ private String lang;
+
+ public AdHocCommandData() {
+ }
+
+ @Override
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<command xmlns=\"http://jabber.org/protocol/commands\"");
+ buf.append(" node=\"").append(node).append("\"");
+ if (sessionID != null) {
+ if (!sessionID.equals("")) {
+ buf.append(" sessionid=\"").append(sessionID).append("\"");
+ }
+ }
+ if (status != null) {
+ buf.append(" status=\"").append(status).append("\"");
+ }
+ if (action != null) {
+ buf.append(" action=\"").append(action).append("\"");
+ }
+
+ if (lang != null) {
+ if (!lang.equals("")) {
+ buf.append(" lang=\"").append(lang).append("\"");
+ }
+ }
+ buf.append(">");
+
+ if (getType() == Type.RESULT) {
+ buf.append("<actions");
+
+ if (executeAction != null) {
+ buf.append(" execute=\"").append(executeAction).append("\"");
+ }
+ if (actions.size() == 0) {
+ buf.append("/>");
+ } else {
+ buf.append(">");
+
+ for (AdHocCommand.Action action : actions) {
+ buf.append("<").append(action).append("/>");
+ }
+ buf.append("</actions>");
+ }
+ }
+
+ if (form != null) {
+ buf.append(form.toXML());
+ }
+
+ for (AdHocCommandNote note : notes) {
+ buf.append("<note type=\"").append(note.getType().toString()).append("\">");
+ buf.append(note.getValue());
+ buf.append("</note>");
+ }
+
+ // TODO ERRORS
+// if (getError() != null) {
+// buf.append(getError().toXML());
+// }
+
+ buf.append("</command>");
+ return buf.toString();
+ }
+
+ /**
+ * Returns the JID of the command host.
+ *
+ * @return the JID of the command host.
+ */
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ /**
+ * Returns the human name of the command
+ *
+ * @return the name of the command.
+ */
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the identifier of the command
+ *
+ * @return the node.
+ */
+ public String getNode() {
+ return node;
+ }
+
+ public void setNode(String node) {
+ this.node = node;
+ }
+
+ /**
+ * Returns the list of notes that the command has.
+ *
+ * @return the notes.
+ */
+ public List<AdHocCommandNote> getNotes() {
+ return notes;
+ }
+
+ public void addNote(AdHocCommandNote note) {
+ this.notes.add(note);
+ }
+
+ public void remveNote(AdHocCommandNote note) {
+ this.notes.remove(note);
+ }
+
+ /**
+ * Returns the form of the command.
+ *
+ * @return the data form associated with the command.
+ */
+ public DataForm getForm() {
+ return form;
+ }
+
+ public void setForm(DataForm form) {
+ this.form = form;
+ }
+
+ /**
+ * Returns the action to execute. The action is set only on a request.
+ *
+ * @return the action to execute.
+ */
+ public AdHocCommand.Action getAction() {
+ return action;
+ }
+
+ public void setAction(AdHocCommand.Action action) {
+ this.action = action;
+ }
+
+ /**
+ * Returns the status of the execution.
+ *
+ * @return the status.
+ */
+ public AdHocCommand.Status getStatus() {
+ return status;
+ }
+
+ public void setStatus(AdHocCommand.Status status) {
+ this.status = status;
+ }
+
+ public List<Action> getActions() {
+ return actions;
+ }
+
+ public void addAction(Action action) {
+ actions.add(action);
+ }
+
+ public void setExecuteAction(Action executeAction) {
+ this.executeAction = executeAction;
+ }
+
+ public Action getExecuteAction() {
+ return executeAction;
+ }
+
+ public void setSessionID(String sessionID) {
+ this.sessionID = sessionID;
+ }
+
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ public static class SpecificError implements PacketExtension {
+
+ public static final String namespace = "http://jabber.org/protocol/commands";
+
+ public SpecificErrorCondition condition;
+
+ public SpecificError(SpecificErrorCondition condition) {
+ this.condition = condition;
+ }
+
+ public String getElementName() {
+ return condition.toString();
+ }
+ public String getNamespace() {
+ return namespace;
+ }
+
+ public SpecificErrorCondition getCondition() {
+ return condition;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<").append(getElementName());
+ buf.append(" xmlns=\"").append(getNamespace()).append("\"/>");
+ return buf.toString();
+ }
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/packet/AttentionExtension.java b/src/org/jivesoftware/smackx/packet/AttentionExtension.java new file mode 100644 index 0000000..d86fa41 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/AttentionExtension.java @@ -0,0 +1,100 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2010 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * A PacketExtension that implements XEP-0224: Attention
+ *
+ * This extension is expected to be added to message stanzas of type 'headline.'
+ * Please refer to the XEP for more implementation guidelines.
+ *
+ * @author Guus der Kinderen, guus.der.kinderen@gmail.com
+ * @see <a
+ * href="http://xmpp.org/extensions/xep-0224.html">XEP-0224: Attention</a>
+ */
+public class AttentionExtension implements PacketExtension {
+
+ /**
+ * The XML element name of an 'attention' extension.
+ */
+ public static final String ELEMENT_NAME = "attention";
+
+ /**
+ * The namespace that qualifies the XML element of an 'attention' extension.
+ */
+ public static final String NAMESPACE = "urn:xmpp:attention:0";
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smack.packet.PacketExtension#getElementName()
+ */
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smack.packet.PacketExtension#getNamespace()
+ */
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smack.packet.PacketExtension#toXML()
+ */
+ public String toXML() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("<").append(getElementName()).append(" xmlns=\"").append(
+ getNamespace()).append("\"/>");
+ return sb.toString();
+ }
+
+ /**
+ * A {@link PacketExtensionProvider} for the {@link AttentionExtension}. As
+ * Attention elements have no state/information other than the element name
+ * and namespace, this implementation simply returns new instances of
+ * {@link AttentionExtension}.
+ *
+ * @author Guus der Kinderen, guus.der.kinderen@gmail.com
+s */
+ public static class Provider implements PacketExtensionProvider {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * org.jivesoftware.smack.provider.PacketExtensionProvider#parseExtension
+ * (org.xmlpull.v1.XmlPullParser)
+ */
+ public PacketExtension parseExtension(XmlPullParser arg0)
+ throws Exception {
+ return new AttentionExtension();
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/packet/ChatStateExtension.java b/src/org/jivesoftware/smackx/packet/ChatStateExtension.java new file mode 100644 index 0000000..ecc6acc --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/ChatStateExtension.java @@ -0,0 +1,73 @@ +/**
+ * $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.smackx.packet;
+
+import org.jivesoftware.smackx.ChatState;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Represents a chat state which is an extension to message packets which is used to indicate
+ * the current status of a chat participant.
+ *
+ * @author Alexander Wenckus
+ * @see org.jivesoftware.smackx.ChatState
+ */
+public class ChatStateExtension implements PacketExtension {
+
+ private ChatState state;
+
+ /**
+ * Default constructor. The argument provided is the state that the extension will represent.
+ *
+ * @param state the state that the extension represents.
+ */
+ public ChatStateExtension(ChatState state) {
+ this.state = state;
+ }
+
+ public String getElementName() {
+ return state.name();
+ }
+
+ public String getNamespace() {
+ return "http://jabber.org/protocol/chatstates";
+ }
+
+ public String toXML() {
+ return "<" + getElementName() + " xmlns=\"" + getNamespace() + "\" />";
+ }
+
+ public static class Provider implements PacketExtensionProvider {
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ ChatState state;
+ try {
+ state = ChatState.valueOf(parser.getName());
+ }
+ catch (Exception ex) {
+ state = ChatState.active;
+ }
+ return new ChatStateExtension(state);
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/packet/DataForm.java b/src/org/jivesoftware/smackx/packet/DataForm.java new file mode 100644 index 0000000..4d12892 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/DataForm.java @@ -0,0 +1,312 @@ +/** + * $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.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.FormField; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Represents a form that could be use for gathering data as well as for reporting data + * returned from a search. + * + * @author Gaston Dombiak + */ +public class DataForm implements PacketExtension { + + private String type; + private String title; + private List<String> instructions = new ArrayList<String>(); + private ReportedData reportedData; + private final List<Item> items = new ArrayList<Item>(); + private final List<FormField> fields = new ArrayList<FormField>(); + + public DataForm(String type) { + this.type = type; + } + + /** + * Returns the meaning of the data within the context. The data could be part of a form + * to fill out, a form submission or data results.<p> + * + * Possible form types are: + * <ul> + * <li>form -> This packet contains a form to fill out. Display it to the user (if your + * program can).</li> + * <li>submit -> The form is filled out, and this is the data that is being returned from + * the form.</li> + * <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li> + * <li>result -> Data results being returned from a search, or some other query.</li> + * </ul> + * + * @return the form's type. + */ + public String getType() { + return type; + } + + /** + * Returns the description of the data. It is similar to the title on a web page or an X + * window. You can put a <title/> on either a form to fill out, or a set of data results. + * + * @return description of the data. + */ + public String getTitle() { + return title; + } + + /** + * Returns an Iterator for the list of instructions that explain how to fill out the form and + * what the form is about. The dataform could include multiple instructions since each + * instruction could not contain newlines characters. Join the instructions together in order + * to show them to the user. + * + * @return an Iterator for the list of instructions that explain how to fill out the form. + */ + public Iterator<String> getInstructions() { + synchronized (instructions) { + return Collections.unmodifiableList(new ArrayList<String>(instructions)).iterator(); + } + } + + /** + * Returns the fields that will be returned from a search. + * + * @return fields that will be returned from a search. + */ + public ReportedData getReportedData() { + return reportedData; + } + + /** + * Returns an Iterator for the items returned from a search. + * + * @return an Iterator for the items returned from a search. + */ + public Iterator<Item> getItems() { + synchronized (items) { + return Collections.unmodifiableList(new ArrayList<Item>(items)).iterator(); + } + } + + /** + * Returns an Iterator for the fields that are part of the form. + * + * @return an Iterator for the fields that are part of the form. + */ + public Iterator<FormField> getFields() { + synchronized (fields) { + return Collections.unmodifiableList(new ArrayList<FormField>(fields)).iterator(); + } + } + + public String getElementName() { + return Form.ELEMENT; + } + + public String getNamespace() { + return Form.NAMESPACE; + } + + /** + * Sets the description of the data. It is similar to the title on a web page or an X window. + * You can put a <title/> on either a form to fill out, or a set of data results. + * + * @param title description of the data. + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * Sets the list of instructions that explain how to fill out the form and what the form is + * about. The dataform could include multiple instructions since each instruction could not + * contain newlines characters. + * + * @param instructions list of instructions that explain how to fill out the form. + */ + public void setInstructions(List<String> instructions) { + this.instructions = instructions; + } + + /** + * Sets the fields that will be returned from a search. + * + * @param reportedData the fields that will be returned from a search. + */ + public void setReportedData(ReportedData reportedData) { + this.reportedData = reportedData; + } + + /** + * Adds a new field as part of the form. + * + * @param field the field to add to the form. + */ + public void addField(FormField field) { + synchronized (fields) { + fields.add(field); + } + } + + /** + * Adds a new instruction to the list of instructions that explain how to fill out the form + * and what the form is about. The dataform could include multiple instructions since each + * instruction could not contain newlines characters. + * + * @param instruction the new instruction that explain how to fill out the form. + */ + public void addInstruction(String instruction) { + synchronized (instructions) { + instructions.add(instruction); + } + } + + /** + * Adds a new item returned from a search. + * + * @param item the item returned from a search. + */ + public void addItem(Item item) { + synchronized (items) { + items.add(item); + } + } + + /** + * Returns true if this DataForm has at least one FORM_TYPE field which is + * hidden. This method is used for sanity checks. + * + * @return + */ + public boolean hasHiddenFormTypeField() { + boolean found = false; + for (FormField f : fields) { + if (f.getVariable().equals("FORM_TYPE") && f.getType() != null && f.getType().equals("hidden")) + found = true; + } + return found; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\" type=\"" + getType() +"\">"); + if (getTitle() != null) { + buf.append("<title>").append(getTitle()).append("</title>"); + } + for (Iterator<String> it=getInstructions(); it.hasNext();) { + buf.append("<instructions>").append(it.next()).append("</instructions>"); + } + // Append the list of fields returned from a search + if (getReportedData() != null) { + buf.append(getReportedData().toXML()); + } + // Loop through all the items returned from a search and append them to the string buffer + for (Iterator<Item> i = getItems(); i.hasNext();) { + Item item = i.next(); + buf.append(item.toXML()); + } + // Loop through all the form fields and append them to the string buffer + for (Iterator<FormField> i = getFields(); i.hasNext();) { + FormField field = i.next(); + buf.append(field.toXML()); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + /** + * + * Represents the fields that will be returned from a search. This information is useful when + * you try to use the jabber:iq:search namespace to return dynamic form information. + * + * @author Gaston Dombiak + */ + public static class ReportedData { + private List<FormField> fields = new ArrayList<FormField>(); + + public ReportedData(List<FormField> fields) { + this.fields = fields; + } + + /** + * Returns the fields returned from a search. + * + * @return the fields returned from a search. + */ + public Iterator<FormField> getFields() { + return Collections.unmodifiableList(new ArrayList<FormField>(fields)).iterator(); + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<reported>"); + // Loop through all the form items and append them to the string buffer + for (Iterator<FormField> i = getFields(); i.hasNext();) { + FormField field = i.next(); + buf.append(field.toXML()); + } + buf.append("</reported>"); + return buf.toString(); + } + } + + /** + * + * Represents items of reported data. + * + * @author Gaston Dombiak + */ + public static class Item { + private List<FormField> fields = new ArrayList<FormField>(); + + public Item(List<FormField> fields) { + this.fields = fields; + } + + /** + * Returns the fields that define the data that goes with the item. + * + * @return the fields that define the data that goes with the item. + */ + public Iterator<FormField> getFields() { + return Collections.unmodifiableList(new ArrayList<FormField>(fields)).iterator(); + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<item>"); + // Loop through all the form items and append them to the string buffer + for (Iterator<FormField> i = getFields(); i.hasNext();) { + FormField field = i.next(); + buf.append(field.toXML()); + } + buf.append("</item>"); + return buf.toString(); + } + } +} diff --git a/src/org/jivesoftware/smackx/packet/DefaultPrivateData.java b/src/org/jivesoftware/smackx/packet/DefaultPrivateData.java new file mode 100644 index 0000000..b58fc6c --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/DefaultPrivateData.java @@ -0,0 +1,137 @@ +/** + * $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.smackx.packet; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * Default implementation of the PrivateData interface. Unless a PrivateDataProvider + * is registered with the PrivateDataManager class, instances of this class will be + * returned when getting private data.<p> + * + * This class provides a very simple representation of an XML sub-document. Each element + * is a key in a Map with its CDATA being the value. For example, given the following + * XML sub-document: + * + * <pre> + * <foo xmlns="http://bar.com"> + * <color>blue</color> + * <food>pizza</food> + * </foo></pre> + * + * In this case, getValue("color") would return "blue", and getValue("food") would + * return "pizza". This parsing mechanism mechanism is very simplistic and will not work + * as desired in all cases (for example, if some of the elements have attributes. In those + * cases, a custom {@link org.jivesoftware.smackx.provider.PrivateDataProvider} should be used. + * + * @author Matt Tucker + */ +public class DefaultPrivateData implements PrivateData { + + private String elementName; + private String namespace; + private Map<String, String> map; + + /** + * Creates a new generic private data object. + * + * @param elementName the name of the element of the XML sub-document. + * @param namespace the namespace of the element. + */ + public DefaultPrivateData(String elementName, String namespace) { + this.elementName = elementName; + this.namespace = namespace; + } + + /** + * Returns the XML element name of the private data sub-packet root element. + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return elementName; + } + + /** + * Returns the XML namespace of the private data sub-packet root element. + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return namespace; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\">"); + for (Iterator<String> i=getNames(); i.hasNext(); ) { + String name = i.next(); + String value = getValue(name); + buf.append("<").append(name).append(">"); + buf.append(value); + buf.append("</").append(name).append(">"); + } + buf.append("</").append(elementName).append(">"); + return buf.toString(); + } + + /** + * Returns an Iterator for the names that can be used to get + * values of the private data. + * + * @return an Iterator for the names. + */ + public synchronized Iterator<String> getNames() { + if (map == null) { + return Collections.<String>emptyList().iterator(); + } + return Collections.unmodifiableSet(map.keySet()).iterator(); + } + + /** + * Returns a value given a name. + * + * @param name the name. + * @return the value. + */ + public synchronized String getValue(String name) { + if (map == null) { + return null; + } + return (String)map.get(name); + } + + /** + * Sets a value given the name. + * + * @param name the name. + * @param value the value. + */ + public synchronized void setValue(String name, String value) { + if (map == null) { + map = new HashMap<String,String>(); + } + map.put(name, value); + } +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/packet/DelayInfo.java b/src/org/jivesoftware/smackx/packet/DelayInfo.java new file mode 100644 index 0000000..f404971 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/DelayInfo.java @@ -0,0 +1,105 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.packet; + +import java.util.Date; + +import org.jivesoftware.smack.util.StringUtils;
+ +/** + * A decorator for the {@link DelayInformation} class to transparently support + * both the new <b>Delay Delivery</b> specification <a href="http://xmpp.org/extensions/xep-0203.html">XEP-0203</a> and + * the old one <a href="http://xmpp.org/extensions/xep-0091.html">XEP-0091</a>. + * + * Existing code can be backward compatible. + * + * @author Robin Collier + */ +public class DelayInfo extends DelayInformation +{ + + DelayInformation wrappedInfo; + + /** + * Creates a new instance with given delay information. + * @param delay the delay information + */ + public DelayInfo(DelayInformation delay) + { + super(delay.getStamp()); + wrappedInfo = delay; + } + + @Override + public String getFrom() + { + return wrappedInfo.getFrom(); + } + + @Override + public String getReason() + { + return wrappedInfo.getReason(); + } + + @Override + public Date getStamp() + { + return wrappedInfo.getStamp(); + } + + @Override + public void setFrom(String from) + { + wrappedInfo.setFrom(from); + } + + @Override + public void setReason(String reason) + { + wrappedInfo.setReason(reason); + } + + @Override + public String getElementName() + { + return "delay"; + } + + @Override + public String getNamespace() + { + return "urn:xmpp:delay"; + } + + @Override + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\""); + buf.append(" stamp=\""); + buf.append(StringUtils.formatXEP0082Date(getStamp()));
+ buf.append("\""); + if (getFrom() != null && getFrom().length() > 0) { + buf.append(" from=\"").append(getFrom()).append("\""); + } + buf.append(">"); + if (getReason() != null && getReason().length() > 0) { + buf.append(getReason()); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + +} diff --git a/src/org/jivesoftware/smackx/packet/DelayInformation.java b/src/org/jivesoftware/smackx/packet/DelayInformation.java new file mode 100644 index 0000000..b9ab485 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/DelayInformation.java @@ -0,0 +1,149 @@ +/** + * $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.smackx.packet; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +import org.jivesoftware.smack.packet.PacketExtension; + +/** + * Represents timestamp information about data stored for later delivery. A DelayInformation will + * always includes the timestamp when the packet was originally sent and may include more + * information such as the JID of the entity that originally sent the packet as well as the reason + * for the delay.<p> + * + * For more information see <a href="http://xmpp.org/extensions/xep-0091.html">XEP-0091</a> + * and <a href="http://xmpp.org/extensions/xep-0203.html">XEP-0203</a>. + * + * @author Gaston Dombiak + */ +public class DelayInformation implements PacketExtension { + + /** + * Date format according to the obsolete XEP-0091 specification. + * XEP-0091 recommends to use this old format for date-time instead of + * the one specified in XEP-0082. + * <p> + * Date formats are not synchronized. Since multiple threads access the format concurrently, + * it must be synchronized externally. + */ + public static final DateFormat XEP_0091_UTC_FORMAT = new SimpleDateFormat( + "yyyyMMdd'T'HH:mm:ss"); + static { + XEP_0091_UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + private Date stamp; + private String from; + private String reason; + + /** + * Creates a new instance with the specified timestamp. + * @param stamp the timestamp + */ + public DelayInformation(Date stamp) { + super(); + this.stamp = stamp; + } + + /** + * Returns the JID of the entity that originally sent the packet or that delayed the + * delivery of the packet or <tt>null</tt> if this information is not available. + * + * @return the JID of the entity that originally sent the packet or that delayed the + * delivery of the packet. + */ + public String getFrom() { + return from; + } + + /** + * Sets the JID of the entity that originally sent the packet or that delayed the + * delivery of the packet or <tt>null</tt> if this information is not available. + * + * @param from the JID of the entity that originally sent the packet. + */ + public void setFrom(String from) { + this.from = from; + } + + /** + * Returns the timestamp when the packet was originally sent. The returned Date is + * be understood as UTC. + * + * @return the timestamp when the packet was originally sent. + */ + public Date getStamp() { + return stamp; + } + + /** + * Returns a natural-language description of the reason for the delay or <tt>null</tt> if + * this information is not available. + * + * @return a natural-language description of the reason for the delay or <tt>null</tt>. + */ + public String getReason() { + return reason; + } + + /** + * Sets a natural-language description of the reason for the delay or <tt>null</tt> if + * this information is not available. + * + * @param reason a natural-language description of the reason for the delay or <tt>null</tt>. + */ + public void setReason(String reason) { + this.reason = reason; + } + + public String getElementName() { + return "x"; + } + + public String getNamespace() { + return "jabber:x:delay"; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\""); + buf.append(" stamp=\""); + synchronized (XEP_0091_UTC_FORMAT) { + buf.append(XEP_0091_UTC_FORMAT.format(stamp)); + } + buf.append("\""); + if (from != null && from.length() > 0) { + buf.append(" from=\"").append(from).append("\""); + } + buf.append(">"); + if (reason != null && reason.length() > 0) { + buf.append(reason); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + +} diff --git a/src/org/jivesoftware/smackx/packet/DiscoverInfo.java b/src/org/jivesoftware/smackx/packet/DiscoverInfo.java new file mode 100644 index 0000000..ba873a9 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/DiscoverInfo.java @@ -0,0 +1,507 @@ +/** + * $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.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.util.StringUtils; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A DiscoverInfo IQ packet, which is used by XMPP clients to request and receive information + * to/from other XMPP entities.<p> + * + * The received information may contain one or more identities of the requested XMPP entity, and + * a list of supported features by the requested XMPP entity. + * + * @author Gaston Dombiak + */ +public class DiscoverInfo extends IQ { + + public static final String NAMESPACE = "http://jabber.org/protocol/disco#info"; + + private final List<Feature> features = new CopyOnWriteArrayList<Feature>(); + private final List<Identity> identities = new CopyOnWriteArrayList<Identity>(); + private String node; + + public DiscoverInfo() { + super(); + } + + /** + * Copy constructor + * + * @param d + */ + public DiscoverInfo(DiscoverInfo d) { + super(d); + + // Set node + setNode(d.getNode()); + + // Copy features + synchronized (d.features) { + for (Feature f : d.features) { + addFeature(f); + } + } + + // Copy identities + synchronized (d.identities) { + for (Identity i : d.identities) { + addIdentity(i); + } + } + } + + /** + * Adds a new feature to the discovered information. + * + * @param feature the discovered feature + */ + public void addFeature(String feature) { + addFeature(new Feature(feature)); + } + + /** + * Adds a collection of features to the packet. Does noting if featuresToAdd is null. + * + * @param featuresToAdd + */ + public void addFeatures(Collection<String> featuresToAdd) { + if (featuresToAdd == null) return; + for (String feature : featuresToAdd) { + addFeature(feature); + } + } + + private void addFeature(Feature feature) { + synchronized (features) { + features.add(feature); + } + } + + /** + * Returns the discovered features of an XMPP entity. + * + * @return an Iterator on the discovered features of an XMPP entity + */ + public Iterator<Feature> getFeatures() { + synchronized (features) { + return Collections.unmodifiableList(features).iterator(); + } + } + + /** + * Adds a new identity of the requested entity to the discovered information. + * + * @param identity the discovered entity's identity + */ + public void addIdentity(Identity identity) { + synchronized (identities) { + identities.add(identity); + } + } + + /** + * Adds identities to the DiscoverInfo stanza + * + * @param identitiesToAdd + */ + public void addIdentities(Collection<Identity> identitiesToAdd) { + if (identitiesToAdd == null) return; + synchronized (identities) { + identities.addAll(identitiesToAdd); + } + } + + /** + * Returns the discovered identities of an XMPP entity. + * + * @return an Iterator on the discoveted identities + */ + public Iterator<Identity> getIdentities() { + synchronized (identities) { + return Collections.unmodifiableList(identities).iterator(); + } + } + + /** + * Returns the node attribute that supplements the 'jid' attribute. A node is merely + * something that is associated with a JID and for which the JID can provide information.<p> + * + * Node attributes SHOULD be used only when trying to provide or query information which + * is not directly addressable. + * + * @return the node attribute that supplements the 'jid' attribute + */ + public String getNode() { + return node; + } + + /** + * Sets the node attribute that supplements the 'jid' attribute. A node is merely + * something that is associated with a JID and for which the JID can provide information.<p> + * + * Node attributes SHOULD be used only when trying to provide or query information which + * is not directly addressable. + * + * @param node the node attribute that supplements the 'jid' attribute + */ + public void setNode(String node) { + this.node = node; + } + + /** + * Returns true if the specified feature is part of the discovered information. + * + * @param feature the feature to check + * @return true if the requestes feature has been discovered + */ + public boolean containsFeature(String feature) { + for (Iterator<Feature> it = getFeatures(); it.hasNext();) { + if (feature.equals(it.next().getVar())) + return true; + } + return false; + } + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<query xmlns=\"" + NAMESPACE + "\""); + if (getNode() != null) { + buf.append(" node=\""); + buf.append(StringUtils.escapeForXML(getNode())); + buf.append("\""); + } + buf.append(">"); + synchronized (identities) { + for (Identity identity : identities) { + buf.append(identity.toXML()); + } + } + synchronized (features) { + for (Feature feature : features) { + buf.append(feature.toXML()); + } + } + // Add packet extensions, if any are defined. + buf.append(getExtensionsXML()); + buf.append("</query>"); + return buf.toString(); + } + + /** + * Test if a DiscoverInfo response contains duplicate identities. + * + * @return true if duplicate identities where found, otherwise false + */ + public boolean containsDuplicateIdentities() { + List<Identity> checkedIdentities = new LinkedList<Identity>(); + for (Identity i : identities) { + for (Identity i2 : checkedIdentities) { + if (i.equals(i2)) + return true; + } + checkedIdentities.add(i); + } + return false; + } + + /** + * Test if a DiscoverInfo response contains duplicate features. + * + * @return true if duplicate identities where found, otherwise false + */ + public boolean containsDuplicateFeatures() { + List<Feature> checkedFeatures = new LinkedList<Feature>(); + for (Feature f : features) { + for (Feature f2 : checkedFeatures) { + if (f.equals(f2)) + return true; + } + checkedFeatures.add(f); + } + return false; + } + + /** + * Represents the identity of a given XMPP entity. An entity may have many identities but all + * the identities SHOULD have the same name.<p> + * + * Refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> + * in order to get the official registry of values for the <i>category</i> and <i>type</i> + * attributes. + * + */ + public static class Identity implements Comparable<Identity> { + + private String category; + private String name; + private String type; + private String lang; // 'xml:lang; + + /** + * Creates a new identity for an XMPP entity. + * + * @param category the entity's category. + * @param name the entity's name. + * @deprecated As per the spec, the type field is mandatory and the 3 argument constructor should be used instead. + */ + public Identity(String category, String name) { + this.category = category; + this.name = name; + } + + /** + * Creates a new identity for an XMPP entity. + * 'category' and 'type' are required by + * <a href="http://xmpp.org/extensions/xep-0030.html#schemas">XEP-30 XML Schemas</a> + * + * @param category the entity's category (required as per XEP-30). + * @param name the entity's name. + * @param type the entity's type (required as per XEP-30). + */ + public Identity(String category, String name, String type) { + if ((category == null) || (type == null)) + throw new IllegalArgumentException("category and type cannot be null"); + + this.category = category; + this.name = name; + this.type = type; + } + + /** + * Returns the entity's category. To get the official registry of values for the + * 'category' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> + * + * @return the entity's category. + */ + public String getCategory() { + return category; + } + + /** + * Returns the identity's name. + * + * @return the identity's name. + */ + public String getName() { + return name; + } + + /** + * Returns the entity's type. To get the official registry of values for the + * 'type' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> + * + * @return the entity's type. + */ + public String getType() { + return type; + } + + /** + * Sets the entity's type. To get the official registry of values for the + * 'type' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> + * + * @param type the identity's type. + * @deprecated As per the spec, this field is mandatory and the 3 argument constructor should be used instead. + */ + public void setType(String type) { + this.type = type; + } + + /** + * Sets the natural language (xml:lang) for this identity (optional) + * + * @param lang the xml:lang of this Identity + */ + public void setLanguage(String lang) { + this.lang = lang; + } + + /** + * Returns the identities natural language if one is set + * + * @return the value of xml:lang of this Identity + */ + public String getLanguage() { + return lang; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<identity"); + // Check if this packet has 'lang' set and maybe append it to the resulting string + if (lang != null) + buf.append(" xml:lang=\"").append(StringUtils.escapeForXML(lang)).append("\""); + // Category must always be set + buf.append(" category=\"").append(StringUtils.escapeForXML(category)).append("\""); + // Name must always be set + buf.append(" name=\"").append(StringUtils.escapeForXML(name)).append("\""); + // Check if this packet has 'type' set and maybe append it to the resulting string + if (type != null) { + buf.append(" type=\"").append(StringUtils.escapeForXML(type)).append("\""); + } + buf.append("/>"); + return buf.toString(); + } + + /** + * Check equality for Identity for category, type, lang and name + * in that order as defined by + * <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0015 5.4 Processing Method (Step 3.3)</a> + * + */ + public boolean equals(Object obj) { + if (obj == null) + return false; + if (obj == this) + return true; + if (obj.getClass() != getClass()) + return false; + + DiscoverInfo.Identity other = (DiscoverInfo.Identity) obj; + if (!this.category.equals(other.category)) + return false; + + String otherLang = other.lang == null ? "" : other.lang; + String thisLang = lang == null ? "" : lang; + if (!otherLang.equals(thisLang)) + return false; + + // This safeguard can be removed once the deprecated constructor is removed. + String otherType = other.type == null ? "" : other.type; + String thisType = type == null ? "" : type; + if (!otherType.equals(thisType)) + return false; + + String otherName = other.name == null ? "" : other.name; + String thisName = name == null ? "" : other.name; + if (!thisName.equals(otherName)) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = 1; + result = 37 * result + category.hashCode(); + result = 37 * result + (lang == null ? 0 : lang.hashCode()); + result = 37 * result + (type == null ? 0 : type.hashCode()); + result = 37 * result + (name == null ? 0 : name.hashCode()); + return result; + } + + /** + * Compares this identity with another one. The comparison order is: + * Category, Type, Lang. If all three are identical the other Identity is considered equal. + * Name is not used for comparision, as defined by XEP-0115 + * + * @param obj + * @return + */ + public int compareTo(DiscoverInfo.Identity other) { + String otherLang = other.lang == null ? "" : other.lang; + String thisLang = lang == null ? "" : lang; + + // This can be removed once the deprecated constructor is removed. + String otherType = other.type == null ? "" : other.type; + String thisType = type == null ? "" : type; + + if (category.equals(other.category)) { + if (thisType.equals(otherType)) { + if (thisLang.equals(otherLang)) { + // Don't compare on name, XEP-30 says that name SHOULD + // be equals for all identities of an entity + return 0; + } else { + return thisLang.compareTo(otherLang); + } + } else { + return thisType.compareTo(otherType); + } + } else { + return category.compareTo(other.category); + } + } + } + + /** + * Represents the features offered by the item. This information helps requestors determine + * what actions are possible with regard to this item (registration, search, join, etc.) + * as well as specific feature types of interest, if any (e.g., for the purpose of feature + * negotiation). + */ + public static class Feature { + + private String variable; + + /** + * Creates a new feature offered by an XMPP entity or item. + * + * @param variable the feature's variable. + */ + public Feature(String variable) { + if (variable == null) + throw new IllegalArgumentException("variable cannot be null"); + this.variable = variable; + } + + /** + * Returns the feature's variable. + * + * @return the feature's variable. + */ + public String getVar() { + return variable; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<feature var=\"").append(StringUtils.escapeForXML(variable)).append("\"/>"); + return buf.toString(); + } + + public boolean equals(Object obj) { + if (obj == null) + return false; + if (obj == this) + return true; + if (obj.getClass() != getClass()) + return false; + + DiscoverInfo.Feature other = (DiscoverInfo.Feature) obj; + return variable.equals(other.variable); + } + + @Override + public int hashCode() { + return 37 * variable.hashCode(); + } + } +} diff --git a/src/org/jivesoftware/smackx/packet/DiscoverItems.java b/src/org/jivesoftware/smackx/packet/DiscoverItems.java new file mode 100644 index 0000000..f6a0941 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/DiscoverItems.java @@ -0,0 +1,253 @@ +/** + * $RCSfile$ + * $Revision: 7071 $ + * $Date: 2007-02-12 08:59:05 +0800 (Mon, 12 Feb 2007) $ + * + * 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.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.util.StringUtils; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A DiscoverItems IQ packet, which is used by XMPP clients to request and receive items + * associated with XMPP entities.<p> + * + * The items could also be queried in order to discover if they contain items inside. Some items + * may be addressable by its JID and others may require to be addressed by a JID and a node name. + * + * @author Gaston Dombiak + */ +public class DiscoverItems extends IQ { + + public static final String NAMESPACE = "http://jabber.org/protocol/disco#items"; + + private final List<Item> items = new CopyOnWriteArrayList<Item>(); + private String node; + + /** + * Adds a new item to the discovered information. + * + * @param item the discovered entity's item + */ + public void addItem(Item item) { + synchronized (items) { + items.add(item); + } + } + + /** + * Adds a collection of items to the discovered information. Does nothing if itemsToAdd is null + * + * @param itemsToAdd + */ + public void addItems(Collection<Item> itemsToAdd) { + if (itemsToAdd == null) return; + for (Item i : itemsToAdd) { + addItem(i); + } + } + + /** + * Returns the discovered items of the queried XMPP entity. + * + * @return an Iterator on the discovered entity's items + */ + public Iterator<DiscoverItems.Item> getItems() { + synchronized (items) { + return Collections.unmodifiableList(items).iterator(); + } + } + + /** + * Returns the node attribute that supplements the 'jid' attribute. A node is merely + * something that is associated with a JID and for which the JID can provide information.<p> + * + * Node attributes SHOULD be used only when trying to provide or query information which + * is not directly addressable. + * + * @return the node attribute that supplements the 'jid' attribute + */ + public String getNode() { + return node; + } + + /** + * Sets the node attribute that supplements the 'jid' attribute. A node is merely + * something that is associated with a JID and for which the JID can provide information.<p> + * + * Node attributes SHOULD be used only when trying to provide or query information which + * is not directly addressable. + * + * @param node the node attribute that supplements the 'jid' attribute + */ + public void setNode(String node) { + this.node = node; + } + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<query xmlns=\"" + NAMESPACE + "\""); + if (getNode() != null) { + buf.append(" node=\""); + buf.append(StringUtils.escapeForXML(getNode())); + buf.append("\""); + } + buf.append(">"); + synchronized (items) { + for (Item item : items) { + buf.append(item.toXML()); + } + } + buf.append("</query>"); + return buf.toString(); + } + + /** + * An item is associated with an XMPP Entity, usually thought of a children of the parent + * entity and normally are addressable as a JID.<p> + * + * An item associated with an entity may not be addressable as a JID. In order to handle + * such items, Service Discovery uses an optional 'node' attribute that supplements the + * 'jid' attribute. + */ + public static class Item { + + /** + * Request to create or update the item. + */ + public static final String UPDATE_ACTION = "update"; + + /** + * Request to remove the item. + */ + public static final String REMOVE_ACTION = "remove"; + + private String entityID; + private String name; + private String node; + private String action; + + /** + * Create a new Item associated with a given entity. + * + * @param entityID the id of the entity that contains the item + */ + public Item(String entityID) { + this.entityID = entityID; + } + + /** + * Returns the entity's ID. + * + * @return the entity's ID. + */ + public String getEntityID() { + return entityID; + } + + /** + * Returns the entity's name. + * + * @return the entity's name. + */ + public String getName() { + return name; + } + + /** + * Sets the entity's name. + * + * @param name the entity's name. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the node attribute that supplements the 'jid' attribute. A node is merely + * something that is associated with a JID and for which the JID can provide information.<p> + * + * Node attributes SHOULD be used only when trying to provide or query information which + * is not directly addressable. + * + * @return the node attribute that supplements the 'jid' attribute + */ + public String getNode() { + return node; + } + + /** + * Sets the node attribute that supplements the 'jid' attribute. A node is merely + * something that is associated with a JID and for which the JID can provide information.<p> + * + * Node attributes SHOULD be used only when trying to provide or query information which + * is not directly addressable. + * + * @param node the node attribute that supplements the 'jid' attribute + */ + public void setNode(String node) { + this.node = node; + } + + /** + * Returns the action that specifies the action being taken for this item. Possible action + * values are: "update" and "remove". Update should either create a new entry if the node + * and jid combination does not already exist, or simply update an existing entry. If + * "remove" is used as the action, the item should be removed from persistent storage. + * + * @return the action being taken for this item + */ + public String getAction() { + return action; + } + + /** + * Sets the action that specifies the action being taken for this item. Possible action + * values are: "update" and "remove". Update should either create a new entry if the node + * and jid combination does not already exist, or simply update an existing entry. If + * "remove" is used as the action, the item should be removed from persistent storage. + * + * @param action the action being taken for this item + */ + public void setAction(String action) { + this.action = action; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<item jid=\"").append(entityID).append("\""); + if (name != null) { + buf.append(" name=\"").append(StringUtils.escapeForXML(name)).append("\""); + } + if (node != null) { + buf.append(" node=\"").append(StringUtils.escapeForXML(node)).append("\""); + } + if (action != null) { + buf.append(" action=\"").append(StringUtils.escapeForXML(action)).append("\""); + } + buf.append("/>"); + return buf.toString(); + } + } +} diff --git a/src/org/jivesoftware/smackx/packet/Header.java b/src/org/jivesoftware/smackx/packet/Header.java new file mode 100644 index 0000000..3fa8386 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/Header.java @@ -0,0 +1,59 @@ +/*
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * Represents a <b>Header</b> entry as specified by the <a href="http://xmpp.org/extensions/xep-031.html">Stanza Headers and Internet Metadata (SHIM)</a>
+
+ * @author Robin Collier
+ */
+public class Header implements PacketExtension
+{
+ private String name;
+ private String value;
+
+ public Header(String name, String value)
+ {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public String getValue()
+ {
+ return value;
+ }
+
+ public String getElementName()
+ {
+ return "header";
+ }
+
+ public String getNamespace()
+ {
+ return HeadersExtension.NAMESPACE;
+ }
+
+ public String toXML()
+ {
+ return "<header name='" + name + "'>" + value + "</header>";
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/packet/HeadersExtension.java b/src/org/jivesoftware/smackx/packet/HeadersExtension.java new file mode 100644 index 0000000..78564db --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/HeadersExtension.java @@ -0,0 +1,69 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jivesoftware.smackx.packet;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * Extension representing a list of headers as specified in <a href="http://xmpp.org/extensions/xep-0131">Stanza Headers and Internet Metadata (SHIM)</a>
+ *
+ * @see Header
+ *
+ * @author Robin Collier
+ */
+public class HeadersExtension implements PacketExtension
+{
+ public static final String NAMESPACE = "http://jabber.org/protocol/shim";
+
+ private Collection<Header> headers = Collections.EMPTY_LIST;
+
+ public HeadersExtension(Collection<Header> headerList)
+ {
+ if (headerList != null)
+ headers = headerList;
+ }
+
+ public Collection<Header> getHeaders()
+ {
+ return headers;
+ }
+
+ public String getElementName()
+ {
+ return "headers";
+ }
+
+ public String getNamespace()
+ {
+ return NAMESPACE;
+ }
+
+ public String toXML()
+ {
+ StringBuilder builder = new StringBuilder("<" + getElementName() + " xmlns='" + getNamespace() + "'>");
+
+ for (Header header : headers)
+ {
+ builder.append(header.toXML());
+ }
+ builder.append("</" + getElementName() + '>');
+
+ return builder.toString();
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/packet/LastActivity.java b/src/org/jivesoftware/smackx/packet/LastActivity.java new file mode 100644 index 0000000..6f4f15a --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/LastActivity.java @@ -0,0 +1,164 @@ +/** + * $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.smackx.packet; + +import java.io.IOException; + +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.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.util.StringUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * A last activity IQ for retrieving information about the last activity associated with a Jabber ID. + * LastActivity (XEP-0012) allows for retrieval of how long a particular user has been idle and the + * message the specified when doing so. Use {@link org.jivesoftware.smackx.LastActivityManager} + * to get the last activity of a user. + * + * @author Derek DeMoro + */ +public class LastActivity extends IQ { + + public static final String NAMESPACE = "jabber:iq:last"; + + public long lastActivity = -1; + public String message; + + public LastActivity() { + setType(IQ.Type.GET); + } + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<query xmlns=\"" + NAMESPACE + "\""); + if (lastActivity != -1) { + buf.append(" seconds=\"").append(lastActivity).append("\""); + } + buf.append("></query>"); + return buf.toString(); + } + + + public void setLastActivity(long lastActivity) { + this.lastActivity = lastActivity; + } + + + private void setMessage(String message) { + this.message = message; + } + + /** + * Returns number of seconds that have passed since the user last logged out. + * If the user is offline, 0 will be returned. + * + * @return the number of seconds that have passed since the user last logged out. + */ + public long getIdleTime() { + return lastActivity; + } + + + /** + * Returns the status message of the last unavailable presence received from the user. + * + * @return the status message of the last unavailable presence received from the user + */ + public String getStatusMessage() { + return message; + } + + + /** + * The IQ Provider for LastActivity. + * + * @author Derek DeMoro + */ + public static class Provider implements IQProvider { + + public Provider() { + super(); + } + + public IQ parseIQ(XmlPullParser parser) throws XMPPException, XmlPullParserException { + if (parser.getEventType() != XmlPullParser.START_TAG) { + throw new XMPPException("Parser not in proper position, or bad XML."); + } + + LastActivity lastActivity = new LastActivity(); + String seconds = parser.getAttributeValue("", "seconds"); + String message = null; + try { + message = parser.nextText(); + } catch (IOException e1) { + // Ignore + } + if (seconds != null) { + try { + lastActivity.setLastActivity(Long.parseLong(seconds)); + } catch (NumberFormatException e) { + // Ignore + } + } + + if (message != null) { + lastActivity.setMessage(message); + } + return lastActivity; + } + } + + /** + * Retrieve the last activity of a particular jid. + * @param con the current Connection. + * @param jid the JID of the user. + * @return the LastActivity packet of the jid. + * @throws XMPPException thrown if a server error has occured. + * @deprecated This method only retreives the lapsed time since the last logout of a particular jid. + * Replaced by {@link org.jivesoftware.smackx.LastActivityManager#getLastActivity(Connection, String) getLastActivity} + */ + public static LastActivity getLastActivity(Connection con, String jid) throws XMPPException { + LastActivity activity = new LastActivity(); + jid = StringUtils.parseBareAddress(jid); + activity.setTo(jid); + + PacketCollector collector = con.createPacketCollector(new PacketIDFilter(activity.getPacketID())); + con.sendPacket(activity); + + LastActivity response = (LastActivity) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Cancel the collector. + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from server on status set."); + } + if (response.getError() != null) { + throw new XMPPException(response.getError()); + } + return response; + } +} diff --git a/src/org/jivesoftware/smackx/packet/MUCAdmin.java b/src/org/jivesoftware/smackx/packet/MUCAdmin.java new file mode 100644 index 0000000..75d17e0 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/MUCAdmin.java @@ -0,0 +1,237 @@ +/** + * $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.smackx.packet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.jivesoftware.smack.packet.IQ; + +/** + * IQ packet that serves for kicking users, granting and revoking voice, banning users, + * modifying the ban list, granting and revoking membership and granting and revoking + * moderator privileges. All these operations are scoped by the + * 'http://jabber.org/protocol/muc#admin' namespace. + * + * @author Gaston Dombiak + */ +public class MUCAdmin extends IQ { + + private List<Item> items = new ArrayList<Item>(); + + /** + * Returns an Iterator for item childs that holds information about roles, affiliation, + * jids and nicks. + * + * @return an Iterator for item childs that holds information about roles, affiliation, + * jids and nicks. + */ + public Iterator<Item> getItems() { + synchronized (items) { + return Collections.unmodifiableList(new ArrayList<Item>(items)).iterator(); + } + } + + /** + * Adds an item child that holds information about roles, affiliation, jids and nicks. + * + * @param item the item child that holds information about roles, affiliation, jids and nicks. + */ + public void addItem(Item item) { + synchronized (items) { + items.add(item); + } + } + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<query xmlns=\"http://jabber.org/protocol/muc#admin\">"); + synchronized (items) { + for (int i = 0; i < items.size(); i++) { + Item item = items.get(i); + buf.append(item.toXML()); + } + } + // Add packet extensions, if any are defined. + buf.append(getExtensionsXML()); + buf.append("</query>"); + return buf.toString(); + } + + /** + * Item child that holds information about roles, affiliation, jids and nicks. + * + * @author Gaston Dombiak + */ + public static class Item { + private String actor; + private String reason; + private String affiliation; + private String jid; + private String nick; + private String role; + + /** + * Creates a new item child. + * + * @param affiliation the actor's affiliation to the room + * @param role the privilege level of an occupant within a room. + */ + public Item(String affiliation, String role) { + this.affiliation = affiliation; + this.role = role; + } + + /** + * Returns the actor (JID of an occupant in the room) that was kicked or banned. + * + * @return the JID of an occupant in the room that was kicked or banned. + */ + public String getActor() { + return actor; + } + + /** + * Returns the reason for the item child. The reason is optional and could be used to + * explain the reason why a user (occupant) was kicked or banned. + * + * @return the reason for the item child. + */ + public String getReason() { + return reason; + } + + /** + * Returns the occupant's affiliation to the room. The affiliation is a semi-permanent + * association or connection with a room. The possible affiliations are "owner", "admin", + * "member", and "outcast" (naturally it is also possible to have no affiliation). An + * affiliation lasts across a user's visits to a room. + * + * @return the actor's affiliation to the room + */ + public String getAffiliation() { + return affiliation; + } + + /** + * Returns the <room@service/nick> by which an occupant is identified within the context + * of a room. If the room is non-anonymous, the JID will be included in the item. + * + * @return the room JID by which an occupant is identified within the room. + */ + public String getJid() { + return jid; + } + + /** + * Returns the new nickname of an occupant that is changing his/her nickname. The new + * nickname is sent as part of the unavailable presence. + * + * @return the new nickname of an occupant that is changing his/her nickname. + */ + public String getNick() { + return nick; + } + + /** + * Returns the temporary position or privilege level of an occupant within a room. The + * possible roles are "moderator", "participant", and "visitor" (it is also possible to + * have no defined role). A role lasts only for the duration of an occupant's visit to + * a room. + * + * @return the privilege level of an occupant within a room. + */ + public String getRole() { + return role; + } + + /** + * Sets the actor (JID of an occupant in the room) that was kicked or banned. + * + * @param actor the actor (JID of an occupant in the room) that was kicked or banned. + */ + public void setActor(String actor) { + this.actor = actor; + } + + /** + * Sets the reason for the item child. The reason is optional and could be used to + * explain the reason why a user (occupant) was kicked or banned. + * + * @param reason the reason why a user (occupant) was kicked or banned. + */ + public void setReason(String reason) { + this.reason = reason; + } + + /** + * Sets the <room@service/nick> by which an occupant is identified within the context + * of a room. If the room is non-anonymous, the JID will be included in the item. + * + * @param jid the JID by which an occupant is identified within a room. + */ + public void setJid(String jid) { + this.jid = jid; + } + + /** + * Sets the new nickname of an occupant that is changing his/her nickname. The new + * nickname is sent as part of the unavailable presence. + * + * @param nick the new nickname of an occupant that is changing his/her nickname. + */ + public void setNick(String nick) { + this.nick = nick; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<item"); + if (getAffiliation() != null) { + buf.append(" affiliation=\"").append(getAffiliation()).append("\""); + } + if (getJid() != null) { + buf.append(" jid=\"").append(getJid()).append("\""); + } + if (getNick() != null) { + buf.append(" nick=\"").append(getNick()).append("\""); + } + if (getRole() != null) { + buf.append(" role=\"").append(getRole()).append("\""); + } + if (getReason() == null && getActor() == null) { + buf.append("/>"); + } + else { + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + if (getActor() != null) { + buf.append("<actor jid=\"").append(getActor()).append("\"/>"); + } + buf.append("</item>"); + } + return buf.toString(); + } + }; +} diff --git a/src/org/jivesoftware/smackx/packet/MUCInitialPresence.java b/src/org/jivesoftware/smackx/packet/MUCInitialPresence.java new file mode 100644 index 0000000..d3d2796 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/MUCInitialPresence.java @@ -0,0 +1,223 @@ +/** + * $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.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +/** + * Represents extended presence information whose sole purpose is to signal the ability of + * the occupant to speak the MUC protocol when joining a room. If the room requires a password + * then the MUCInitialPresence should include one.<p> + * + * The amount of discussion history provided on entering a room (perhaps because the + * user is on a low-bandwidth connection or is using a small-footprint client) could be managed by + * setting a configured History instance to the MUCInitialPresence instance. + * @see MUCInitialPresence#setHistory(MUCInitialPresence.History). + * + * @author Gaston Dombiak + */ +public class MUCInitialPresence implements PacketExtension { + + private String password; + private History history; + + public String getElementName() { + return "x"; + } + + public String getNamespace() { + return "http://jabber.org/protocol/muc"; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\">"); + if (getPassword() != null) { + buf.append("<password>").append(getPassword()).append("</password>"); + } + if (getHistory() != null) { + buf.append(getHistory().toXML()); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + /** + * Returns the history that manages the amount of discussion history provided on + * entering a room. + * + * @return the history that manages the amount of discussion history provided on + * entering a room. + */ + public History getHistory() { + return history; + } + + /** + * Returns the password to use when the room requires a password. + * + * @return the password to use when the room requires a password. + */ + public String getPassword() { + return password; + } + + /** + * Sets the History that manages the amount of discussion history provided on + * entering a room. + * + * @param history that manages the amount of discussion history provided on + * entering a room. + */ + public void setHistory(History history) { + this.history = history; + } + + /** + * Sets the password to use when the room requires a password. + * + * @param password the password to use when the room requires a password. + */ + public void setPassword(String password) { + this.password = password; + } + + /** + * The History class controls the number of characters or messages to receive + * when entering a room. + * + * @author Gaston Dombiak + */ + public static class History { + + private int maxChars = -1; + private int maxStanzas = -1; + private int seconds = -1; + private Date since; + + /** + * Returns the total number of characters to receive in the history. + * + * @return total number of characters to receive in the history. + */ + public int getMaxChars() { + return maxChars; + } + + /** + * Returns the total number of messages to receive in the history. + * + * @return the total number of messages to receive in the history. + */ + public int getMaxStanzas() { + return maxStanzas; + } + + /** + * Returns the number of seconds to use to filter the messages received during that time. + * In other words, only the messages received in the last "X" seconds will be included in + * the history. + * + * @return the number of seconds to use to filter the messages received during that time. + */ + public int getSeconds() { + return seconds; + } + + /** + * Returns the since date to use to filter the messages received during that time. + * In other words, only the messages received since the datetime specified will be + * included in the history. + * + * @return the since date to use to filter the messages received during that time. + */ + public Date getSince() { + return since; + } + + /** + * Sets the total number of characters to receive in the history. + * + * @param maxChars the total number of characters to receive in the history. + */ + public void setMaxChars(int maxChars) { + this.maxChars = maxChars; + } + + /** + * Sets the total number of messages to receive in the history. + * + * @param maxStanzas the total number of messages to receive in the history. + */ + public void setMaxStanzas(int maxStanzas) { + this.maxStanzas = maxStanzas; + } + + /** + * Sets the number of seconds to use to filter the messages received during that time. + * In other words, only the messages received in the last "X" seconds will be included in + * the history. + * + * @param seconds the number of seconds to use to filter the messages received during + * that time. + */ + public void setSeconds(int seconds) { + this.seconds = seconds; + } + + /** + * Sets the since date to use to filter the messages received during that time. + * In other words, only the messages received since the datetime specified will be + * included in the history. + * + * @param since the since date to use to filter the messages received during that time. + */ + public void setSince(Date since) { + this.since = since; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<history"); + if (getMaxChars() != -1) { + buf.append(" maxchars=\"").append(getMaxChars()).append("\""); + } + if (getMaxStanzas() != -1) { + buf.append(" maxstanzas=\"").append(getMaxStanzas()).append("\""); + } + if (getSeconds() != -1) { + buf.append(" seconds=\"").append(getSeconds()).append("\""); + } + if (getSince() != null) { + SimpleDateFormat utcFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + utcFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + buf.append(" since=\"").append(utcFormat.format(getSince())).append("\""); + } + buf.append("/>"); + return buf.toString(); + } + } +} diff --git a/src/org/jivesoftware/smackx/packet/MUCOwner.java b/src/org/jivesoftware/smackx/packet/MUCOwner.java new file mode 100644 index 0000000..e33806e --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/MUCOwner.java @@ -0,0 +1,342 @@ +/** + * $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.smackx.packet; +import org.jivesoftware.smack.packet.IQ; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * IQ packet that serves for granting and revoking ownership privileges, granting + * and revoking administrative privileges and destroying a room. All these operations + * are scoped by the 'http://jabber.org/protocol/muc#owner' namespace. + * + * @author Gaston Dombiak + */ +public class MUCOwner extends IQ { + + private List<Item> items = new ArrayList<Item>(); + private Destroy destroy; + + /** + * Returns an Iterator for item childs that holds information about affiliation, + * jids and nicks. + * + * @return an Iterator for item childs that holds information about affiliation, + * jids and nicks. + */ + public Iterator<Item> getItems() { + synchronized (items) { + return Collections.unmodifiableList(new ArrayList<Item>(items)).iterator(); + } + } + + /** + * Returns a request to the server to destroy a room. The sender of the request + * should be the room's owner. If the sender of the destroy request is not the room's owner + * then the server will answer a "Forbidden" error. + * + * @return a request to the server to destroy a room. + */ + public Destroy getDestroy() { + return destroy; + } + + /** + * Sets a request to the server to destroy a room. The sender of the request + * should be the room's owner. If the sender of the destroy request is not the room's owner + * then the server will answer a "Forbidden" error. + * + * @param destroy the request to the server to destroy a room. + */ + public void setDestroy(Destroy destroy) { + this.destroy = destroy; + } + + /** + * Adds an item child that holds information about affiliation, jids and nicks. + * + * @param item the item child that holds information about affiliation, jids and nicks. + */ + public void addItem(Item item) { + synchronized (items) { + items.add(item); + } + } + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<query xmlns=\"http://jabber.org/protocol/muc#owner\">"); + synchronized (items) { + for (int i = 0; i < items.size(); i++) { + Item item = (Item) items.get(i); + buf.append(item.toXML()); + } + } + if (getDestroy() != null) { + buf.append(getDestroy().toXML()); + } + // Add packet extensions, if any are defined. + buf.append(getExtensionsXML()); + buf.append("</query>"); + return buf.toString(); + } + + /** + * Item child that holds information about affiliation, jids and nicks. + * + * @author Gaston Dombiak + */ + public static class Item { + + private String actor; + private String reason; + private String affiliation; + private String jid; + private String nick; + private String role; + + /** + * Creates a new item child. + * + * @param affiliation the actor's affiliation to the room + */ + public Item(String affiliation) { + this.affiliation = affiliation; + } + + /** + * Returns the actor (JID of an occupant in the room) that was kicked or banned. + * + * @return the JID of an occupant in the room that was kicked or banned. + */ + public String getActor() { + return actor; + } + + /** + * Returns the reason for the item child. The reason is optional and could be used to + * explain the reason why a user (occupant) was kicked or banned. + * + * @return the reason for the item child. + */ + public String getReason() { + return reason; + } + + /** + * Returns the occupant's affiliation to the room. The affiliation is a semi-permanent + * association or connection with a room. The possible affiliations are "owner", "admin", + * "member", and "outcast" (naturally it is also possible to have no affiliation). An + * affiliation lasts across a user's visits to a room. + * + * @return the actor's affiliation to the room + */ + public String getAffiliation() { + return affiliation; + } + + /** + * Returns the <room@service/nick> by which an occupant is identified within the context + * of a room. If the room is non-anonymous, the JID will be included in the item. + * + * @return the room JID by which an occupant is identified within the room. + */ + public String getJid() { + return jid; + } + + /** + * Returns the new nickname of an occupant that is changing his/her nickname. The new + * nickname is sent as part of the unavailable presence. + * + * @return the new nickname of an occupant that is changing his/her nickname. + */ + public String getNick() { + return nick; + } + + /** + * Returns the temporary position or privilege level of an occupant within a room. The + * possible roles are "moderator", "participant", and "visitor" (it is also possible to + * have no defined role). A role lasts only for the duration of an occupant's visit to + * a room. + * + * @return the privilege level of an occupant within a room. + */ + public String getRole() { + return role; + } + + /** + * Sets the actor (JID of an occupant in the room) that was kicked or banned. + * + * @param actor the actor (JID of an occupant in the room) that was kicked or banned. + */ + public void setActor(String actor) { + this.actor = actor; + } + + /** + * Sets the reason for the item child. The reason is optional and could be used to + * explain the reason why a user (occupant) was kicked or banned. + * + * @param reason the reason why a user (occupant) was kicked or banned. + */ + public void setReason(String reason) { + this.reason = reason; + } + + /** + * Sets the <room@service/nick> by which an occupant is identified within the context + * of a room. If the room is non-anonymous, the JID will be included in the item. + * + * @param jid the JID by which an occupant is identified within a room. + */ + public void setJid(String jid) { + this.jid = jid; + } + + /** + * Sets the new nickname of an occupant that is changing his/her nickname. The new + * nickname is sent as part of the unavailable presence. + * + * @param nick the new nickname of an occupant that is changing his/her nickname. + */ + public void setNick(String nick) { + this.nick = nick; + } + + /** + * Sets the temporary position or privilege level of an occupant within a room. The + * possible roles are "moderator", "participant", and "visitor" (it is also possible to + * have no defined role). A role lasts only for the duration of an occupant's visit to + * a room. + * + * @param role the new privilege level of an occupant within a room. + */ + public void setRole(String role) { + this.role = role; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<item"); + if (getAffiliation() != null) { + buf.append(" affiliation=\"").append(getAffiliation()).append("\""); + } + if (getJid() != null) { + buf.append(" jid=\"").append(getJid()).append("\""); + } + if (getNick() != null) { + buf.append(" nick=\"").append(getNick()).append("\""); + } + if (getRole() != null) { + buf.append(" role=\"").append(getRole()).append("\""); + } + if (getReason() == null && getActor() == null) { + buf.append("/>"); + } + else { + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + if (getActor() != null) { + buf.append("<actor jid=\"").append(getActor()).append("\"/>"); + } + buf.append("</item>"); + } + return buf.toString(); + } + }; + + /** + * Represents a request to the server to destroy a room. The sender of the request + * should be the room's owner. If the sender of the destroy request is not the room's owner + * then the server will answer a "Forbidden" error. + * + * @author Gaston Dombiak + */ + public static class Destroy { + private String reason; + private String jid; + + + /** + * Returns the JID of an alternate location since the current room is being destroyed. + * + * @return the JID of an alternate location. + */ + public String getJid() { + return jid; + } + + /** + * Returns the reason for the room destruction. + * + * @return the reason for the room destruction. + */ + public String getReason() { + return reason; + } + + /** + * Sets the JID of an alternate location since the current room is being destroyed. + * + * @param jid the JID of an alternate location. + */ + public void setJid(String jid) { + this.jid = jid; + } + + /** + * Sets the reason for the room destruction. + * + * @param reason the reason for the room destruction. + */ + public void setReason(String reason) { + this.reason = reason; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<destroy"); + if (getJid() != null) { + buf.append(" jid=\"").append(getJid()).append("\""); + } + if (getReason() == null) { + buf.append("/>"); + } + else { + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + buf.append("</destroy>"); + } + return buf.toString(); + } + + } +} diff --git a/src/org/jivesoftware/smackx/packet/MUCUser.java b/src/org/jivesoftware/smackx/packet/MUCUser.java new file mode 100644 index 0000000..bfcd67c --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/MUCUser.java @@ -0,0 +1,627 @@ +/** + * $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.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; + +/** + * Represents extended presence information about roles, affiliations, full JIDs, + * or status codes scoped by the 'http://jabber.org/protocol/muc#user' namespace. + * + * @author Gaston Dombiak + */ +public class MUCUser implements PacketExtension { + + private Invite invite; + private Decline decline; + private Item item; + private String password; + private Status status; + private Destroy destroy; + + public String getElementName() { + return "x"; + } + + public String getNamespace() { + return "http://jabber.org/protocol/muc#user"; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\">"); + if (getInvite() != null) { + buf.append(getInvite().toXML()); + } + if (getDecline() != null) { + buf.append(getDecline().toXML()); + } + if (getItem() != null) { + buf.append(getItem().toXML()); + } + if (getPassword() != null) { + buf.append("<password>").append(getPassword()).append("</password>"); + } + if (getStatus() != null) { + buf.append(getStatus().toXML()); + } + if (getDestroy() != null) { + buf.append(getDestroy().toXML()); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + /** + * Returns the invitation for another user to a room. The sender of the invitation + * must be an occupant of the room. The invitation will be sent to the room which in turn + * will forward the invitation to the invitee. + * + * @return an invitation for another user to a room. + */ + public Invite getInvite() { + return invite; + } + + /** + * Returns the rejection to an invitation from another user to a room. The rejection will be + * sent to the room which in turn will forward the refusal to the inviter. + * + * @return a rejection to an invitation from another user to a room. + */ + public Decline getDecline() { + return decline; + } + + /** + * Returns the item child that holds information about roles, affiliation, jids and nicks. + * + * @return an item child that holds information about roles, affiliation, jids and nicks. + */ + public Item getItem() { + return item; + } + + /** + * Returns the password to use to enter Password-Protected Room. A Password-Protected Room is + * a room that a user cannot enter without first providing the correct password. + * + * @return the password to use to enter Password-Protected Room. + */ + public String getPassword() { + return password; + } + + /** + * Returns the status which holds a code that assists in presenting notification messages. + * + * @return the status which holds a code that assists in presenting notification messages. + */ + public Status getStatus() { + return status; + } + + /** + * Returns the notification that the room has been destroyed. After a room has been destroyed, + * the room occupants will receive a Presence packet of type 'unavailable' with the reason for + * the room destruction if provided by the room owner. + * + * @return a notification that the room has been destroyed. + */ + public Destroy getDestroy() { + return destroy; + } + + /** + * Sets the invitation for another user to a room. The sender of the invitation + * must be an occupant of the room. The invitation will be sent to the room which in turn + * will forward the invitation to the invitee. + * + * @param invite the invitation for another user to a room. + */ + public void setInvite(Invite invite) { + this.invite = invite; + } + + /** + * Sets the rejection to an invitation from another user to a room. The rejection will be + * sent to the room which in turn will forward the refusal to the inviter. + * + * @param decline the rejection to an invitation from another user to a room. + */ + public void setDecline(Decline decline) { + this.decline = decline; + } + + /** + * Sets the item child that holds information about roles, affiliation, jids and nicks. + * + * @param item the item child that holds information about roles, affiliation, jids and nicks. + */ + public void setItem(Item item) { + this.item = item; + } + + /** + * Sets the password to use to enter Password-Protected Room. A Password-Protected Room is + * a room that a user cannot enter without first providing the correct password. + * + * @param string the password to use to enter Password-Protected Room. + */ + public void setPassword(String string) { + password = string; + } + + /** + * Sets the status which holds a code that assists in presenting notification messages. + * + * @param status the status which holds a code that assists in presenting notification + * messages. + */ + public void setStatus(Status status) { + this.status = status; + } + + /** + * Sets the notification that the room has been destroyed. After a room has been destroyed, + * the room occupants will receive a Presence packet of type 'unavailable' with the reason for + * the room destruction if provided by the room owner. + * + * @param destroy the notification that the room has been destroyed. + */ + public void setDestroy(Destroy destroy) { + this.destroy = destroy; + } + + /** + * Represents an invitation for another user to a room. The sender of the invitation + * must be an occupant of the room. The invitation will be sent to the room which in turn + * will forward the invitation to the invitee. + * + * @author Gaston Dombiak + */ + public static class Invite { + private String reason; + private String from; + private String to; + + /** + * Returns the bare JID of the inviter or, optionally, the room JID. (e.g. + * 'crone1@shakespeare.lit/desktop'). + * + * @return the room's occupant that sent the invitation. + */ + public String getFrom() { + return from; + } + + /** + * Returns the message explaining the invitation. + * + * @return the message explaining the invitation. + */ + public String getReason() { + return reason; + } + + /** + * Returns the bare JID of the invitee. (e.g. 'hecate@shakespeare.lit') + * + * @return the bare JID of the invitee. + */ + public String getTo() { + return to; + } + + /** + * Sets the bare JID of the inviter or, optionally, the room JID. (e.g. + * 'crone1@shakespeare.lit/desktop') + * + * @param from the bare JID of the inviter or, optionally, the room JID. + */ + public void setFrom(String from) { + this.from = from; + } + + /** + * Sets the message explaining the invitation. + * + * @param reason the message explaining the invitation. + */ + public void setReason(String reason) { + this.reason = reason; + } + + /** + * Sets the bare JID of the invitee. (e.g. 'hecate@shakespeare.lit') + * + * @param to the bare JID of the invitee. + */ + public void setTo(String to) { + this.to = to; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<invite "); + if (getTo() != null) { + buf.append(" to=\"").append(getTo()).append("\""); + } + if (getFrom() != null) { + buf.append(" from=\"").append(getFrom()).append("\""); + } + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + buf.append("</invite>"); + return buf.toString(); + } + } + + /** + * Represents a rejection to an invitation from another user to a room. The rejection will be + * sent to the room which in turn will forward the refusal to the inviter. + * + * @author Gaston Dombiak + */ + public static class Decline { + private String reason; + private String from; + private String to; + + /** + * Returns the bare JID of the invitee that rejected the invitation. (e.g. + * 'crone1@shakespeare.lit/desktop'). + * + * @return the bare JID of the invitee that rejected the invitation. + */ + public String getFrom() { + return from; + } + + /** + * Returns the message explaining why the invitation was rejected. + * + * @return the message explaining the reason for the rejection. + */ + public String getReason() { + return reason; + } + + /** + * Returns the bare JID of the inviter. (e.g. 'hecate@shakespeare.lit') + * + * @return the bare JID of the inviter. + */ + public String getTo() { + return to; + } + + /** + * Sets the bare JID of the invitee that rejected the invitation. (e.g. + * 'crone1@shakespeare.lit/desktop'). + * + * @param from the bare JID of the invitee that rejected the invitation. + */ + public void setFrom(String from) { + this.from = from; + } + + /** + * Sets the message explaining why the invitation was rejected. + * + * @param reason the message explaining the reason for the rejection. + */ + public void setReason(String reason) { + this.reason = reason; + } + + /** + * Sets the bare JID of the inviter. (e.g. 'hecate@shakespeare.lit') + * + * @param to the bare JID of the inviter. + */ + public void setTo(String to) { + this.to = to; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<decline "); + if (getTo() != null) { + buf.append(" to=\"").append(getTo()).append("\""); + } + if (getFrom() != null) { + buf.append(" from=\"").append(getFrom()).append("\""); + } + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + buf.append("</decline>"); + return buf.toString(); + } + } + + /** + * Item child that holds information about roles, affiliation, jids and nicks. + * + * @author Gaston Dombiak + */ + public static class Item { + private String actor; + private String reason; + private String affiliation; + private String jid; + private String nick; + private String role; + + /** + * Creates a new item child. + * + * @param affiliation the actor's affiliation to the room + * @param role the privilege level of an occupant within a room. + */ + public Item(String affiliation, String role) { + this.affiliation = affiliation; + this.role = role; + } + + /** + * Returns the actor (JID of an occupant in the room) that was kicked or banned. + * + * @return the JID of an occupant in the room that was kicked or banned. + */ + public String getActor() { + return actor == null ? "" : actor; + } + + /** + * Returns the reason for the item child. The reason is optional and could be used to + * explain the reason why a user (occupant) was kicked or banned. + * + * @return the reason for the item child. + */ + public String getReason() { + return reason == null ? "" : reason; + } + + /** + * Returns the occupant's affiliation to the room. The affiliation is a semi-permanent + * association or connection with a room. The possible affiliations are "owner", "admin", + * "member", and "outcast" (naturally it is also possible to have no affiliation). An + * affiliation lasts across a user's visits to a room. + * + * @return the actor's affiliation to the room + */ + public String getAffiliation() { + return affiliation; + } + + /** + * Returns the <room@service/nick> by which an occupant is identified within the context + * of a room. If the room is non-anonymous, the JID will be included in the item. + * + * @return the room JID by which an occupant is identified within the room. + */ + public String getJid() { + return jid; + } + + /** + * Returns the new nickname of an occupant that is changing his/her nickname. The new + * nickname is sent as part of the unavailable presence. + * + * @return the new nickname of an occupant that is changing his/her nickname. + */ + public String getNick() { + return nick; + } + + /** + * Returns the temporary position or privilege level of an occupant within a room. The + * possible roles are "moderator", "participant", and "visitor" (it is also possible to + * have no defined role). A role lasts only for the duration of an occupant's visit to + * a room. + * + * @return the privilege level of an occupant within a room. + */ + public String getRole() { + return role; + } + + /** + * Sets the actor (JID of an occupant in the room) that was kicked or banned. + * + * @param actor the actor (JID of an occupant in the room) that was kicked or banned. + */ + public void setActor(String actor) { + this.actor = actor; + } + + /** + * Sets the reason for the item child. The reason is optional and could be used to + * explain the reason why a user (occupant) was kicked or banned. + * + * @param reason the reason why a user (occupant) was kicked or banned. + */ + public void setReason(String reason) { + this.reason = reason; + } + + /** + * Sets the <room@service/nick> by which an occupant is identified within the context + * of a room. If the room is non-anonymous, the JID will be included in the item. + * + * @param jid the JID by which an occupant is identified within a room. + */ + public void setJid(String jid) { + this.jid = jid; + } + + /** + * Sets the new nickname of an occupant that is changing his/her nickname. The new + * nickname is sent as part of the unavailable presence. + * + * @param nick the new nickname of an occupant that is changing his/her nickname. + */ + public void setNick(String nick) { + this.nick = nick; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<item"); + if (getAffiliation() != null) { + buf.append(" affiliation=\"").append(getAffiliation()).append("\""); + } + if (getJid() != null) { + buf.append(" jid=\"").append(getJid()).append("\""); + } + if (getNick() != null) { + buf.append(" nick=\"").append(getNick()).append("\""); + } + if (getRole() != null) { + buf.append(" role=\"").append(getRole()).append("\""); + } + if (getReason() == null && getActor() == null) { + buf.append("/>"); + } + else { + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + if (getActor() != null) { + buf.append("<actor jid=\"").append(getActor()).append("\"/>"); + } + buf.append("</item>"); + } + return buf.toString(); + } + } + + /** + * Status code assists in presenting notification messages. The following link provides the + * list of existing error codes (@link http://www.jabber.org/jeps/jep-0045.html#errorstatus). + * + * @author Gaston Dombiak + */ + public static class Status { + private String code; + + /** + * Creates a new instance of Status with the specified code. + * + * @param code the code that uniquely identifies the reason of the error. + */ + public Status(String code) { + this.code = code; + } + + /** + * Returns the code that uniquely identifies the reason of the error. The code + * assists in presenting notification messages. + * + * @return the code that uniquely identifies the reason of the error. + */ + public String getCode() { + return code; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<status code=\"").append(getCode()).append("\"/>"); + return buf.toString(); + } + } + + /** + * Represents a notification that the room has been destroyed. After a room has been destroyed, + * the room occupants will receive a Presence packet of type 'unavailable' with the reason for + * the room destruction if provided by the room owner. + * + * @author Gaston Dombiak + */ + public static class Destroy { + private String reason; + private String jid; + + + /** + * Returns the JID of an alternate location since the current room is being destroyed. + * + * @return the JID of an alternate location. + */ + public String getJid() { + return jid; + } + + /** + * Returns the reason for the room destruction. + * + * @return the reason for the room destruction. + */ + public String getReason() { + return reason; + } + + /** + * Sets the JID of an alternate location since the current room is being destroyed. + * + * @param jid the JID of an alternate location. + */ + public void setJid(String jid) { + this.jid = jid; + } + + /** + * Sets the reason for the room destruction. + * + * @param reason the reason for the room destruction. + */ + public void setReason(String reason) { + this.reason = reason; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<destroy"); + if (getJid() != null) { + buf.append(" jid=\"").append(getJid()).append("\""); + } + if (getReason() == null) { + buf.append("/>"); + } + else { + buf.append(">"); + if (getReason() != null) { + buf.append("<reason>").append(getReason()).append("</reason>"); + } + buf.append("</destroy>"); + } + return buf.toString(); + } + + } +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/packet/MessageEvent.java b/src/org/jivesoftware/smackx/packet/MessageEvent.java new file mode 100644 index 0000000..5e146dc --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/MessageEvent.java @@ -0,0 +1,335 @@ +/** + * $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.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; + +import java.util.ArrayList; +import java.util.Iterator; + +/** + * Represents message events relating to the delivery, display, composition and cancellation of + * messages.<p> + * + * There are four message events currently defined in this namespace: + * <ol> + * <li>Offline<br> + * Indicates that the message has been stored offline by the intended recipient's server. This + * event is triggered only if the intended recipient's server supports offline storage, has that + * support enabled, and the recipient is offline when the server receives the message for delivery.</li> + * + * <li>Delivered<br> + * Indicates that the message has been delivered to the recipient. This signifies that the message + * has reached the recipient's XMPP client, but does not necessarily mean that the message has + * been displayed. This event is to be raised by the XMPP client.</li> + * + * <li>Displayed<br> + * Once the message has been received by the recipient's XMPP client, it may be displayed to the + * user. This event indicates that the message has been displayed, and is to be raised by the + * XMPP client. Even if a message is displayed multiple times, this event should be raised only + * once.</li> + * + * <li>Composing<br> + * In threaded chat conversations, this indicates that the recipient is composing a reply to a + * message. The event is to be raised by the recipient's XMPP client. A XMPP client is allowed + * to raise this event multiple times in response to the same request, providing the original + * event is cancelled first.</li> + * </ol> + * + * @author Gaston Dombiak + */ +public class MessageEvent implements PacketExtension { + + public static final String OFFLINE = "offline"; + public static final String COMPOSING = "composing"; + public static final String DISPLAYED = "displayed"; + public static final String DELIVERED = "delivered"; + public static final String CANCELLED = "cancelled"; + + private boolean offline = false; + private boolean delivered = false; + private boolean displayed = false; + private boolean composing = false; + private boolean cancelled = true; + + private String packetID = null; + + /** + * Returns the XML element name of the extension sub-packet root element. + * Always returns "x" + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return "x"; + } + + /** + * Returns the XML namespace of the extension sub-packet root element. + * According the specification the namespace is always "jabber:x:event" + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return "jabber:x:event"; + } + + /** + * When the message is a request returns if the sender of the message requests to be notified + * when the receiver is composing a reply. + * When the message is a notification returns if the receiver of the message is composing a + * reply. + * + * @return true if the sender is requesting to be notified when composing or when notifying + * that the receiver of the message is composing a reply + */ + public boolean isComposing() { + return composing; + } + + /** + * When the message is a request returns if the sender of the message requests to be notified + * when the message is delivered. + * When the message is a notification returns if the message was delivered or not. + * + * @return true if the sender is requesting to be notified when delivered or when notifying + * that the message was delivered + */ + public boolean isDelivered() { + return delivered; + } + + /** + * When the message is a request returns if the sender of the message requests to be notified + * when the message is displayed. + * When the message is a notification returns if the message was displayed or not. + * + * @return true if the sender is requesting to be notified when displayed or when notifying + * that the message was displayed + */ + public boolean isDisplayed() { + return displayed; + } + + /** + * When the message is a request returns if the sender of the message requests to be notified + * when the receiver of the message is offline. + * When the message is a notification returns if the receiver of the message was offline. + * + * @return true if the sender is requesting to be notified when offline or when notifying + * that the receiver of the message is offline + */ + public boolean isOffline() { + return offline; + } + + /** + * When the message is a notification returns if the receiver of the message cancelled + * composing a reply. + * + * @return true if the receiver of the message cancelled composing a reply + */ + public boolean isCancelled() { + return cancelled; + } + + /** + * Returns the unique ID of the message that requested to be notified of the event. + * The packet id is not used when the message is a request for notifications + * + * @return the message id that requested to be notified of the event. + */ + public String getPacketID() { + return packetID; + } + + /** + * Returns the types of events. The type of event could be: + * "offline", "composing","delivered","displayed", "offline" + * + * @return an iterator over all the types of events of the MessageEvent. + */ + public Iterator<String> getEventTypes() { + ArrayList<String> allEvents = new ArrayList<String>(); + if (isDelivered()) { + allEvents.add(MessageEvent.DELIVERED); + } + if (!isMessageEventRequest() && isCancelled()) { + allEvents.add(MessageEvent.CANCELLED); + } + if (isComposing()) { + allEvents.add(MessageEvent.COMPOSING); + } + if (isDisplayed()) { + allEvents.add(MessageEvent.DISPLAYED); + } + if (isOffline()) { + allEvents.add(MessageEvent.OFFLINE); + } + return allEvents.iterator(); + } + + /** + * When the message is a request sets if the sender of the message requests to be notified + * when the receiver is composing a reply. + * When the message is a notification sets if the receiver of the message is composing a + * reply. + * + * @param composing sets if the sender is requesting to be notified when composing or when + * notifying that the receiver of the message is composing a reply + */ + public void setComposing(boolean composing) { + this.composing = composing; + setCancelled(false); + } + + /** + * When the message is a request sets if the sender of the message requests to be notified + * when the message is delivered. + * When the message is a notification sets if the message was delivered or not. + * + * @param delivered sets if the sender is requesting to be notified when delivered or when + * notifying that the message was delivered + */ + public void setDelivered(boolean delivered) { + this.delivered = delivered; + setCancelled(false); + } + + /** + * When the message is a request sets if the sender of the message requests to be notified + * when the message is displayed. + * When the message is a notification sets if the message was displayed or not. + * + * @param displayed sets if the sender is requesting to be notified when displayed or when + * notifying that the message was displayed + */ + public void setDisplayed(boolean displayed) { + this.displayed = displayed; + setCancelled(false); + } + + /** + * When the message is a request sets if the sender of the message requests to be notified + * when the receiver of the message is offline. + * When the message is a notification sets if the receiver of the message was offline. + * + * @param offline sets if the sender is requesting to be notified when offline or when + * notifying that the receiver of the message is offline + */ + public void setOffline(boolean offline) { + this.offline = offline; + setCancelled(false); + } + + /** + * When the message is a notification sets if the receiver of the message cancelled + * composing a reply. + * The Cancelled event is never requested explicitly. It is requested implicitly when + * requesting to be notified of the Composing event. + * + * @param cancelled sets if the receiver of the message cancelled composing a reply + */ + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + /** + * Sets the unique ID of the message that requested to be notified of the event. + * The packet id is not used when the message is a request for notifications + * + * @param packetID the message id that requested to be notified of the event. + */ + public void setPacketID(String packetID) { + this.packetID = packetID; + } + + /** + * Returns true if this MessageEvent is a request for notifications. + * Returns false if this MessageEvent is a notification of an event. + * + * @return true if this message is a request for notifications. + */ + public boolean isMessageEventRequest() { + return this.packetID == null; + } + + /** + * Returns the XML representation of a Message Event according the specification. + * + * Usually the XML representation will be inside of a Message XML representation like + * in the following examples:<p> + * + * Request to be notified when displayed: + * <pre> + * <message + * to='romeo@montague.net/orchard' + * from='juliet@capulet.com/balcony' + * id='message22'> + * <x xmlns='jabber:x:event'> + * <displayed/> + * </x> + * </message> + * </pre> + * + * Notification of displayed: + * <pre> + * <message + * from='romeo@montague.net/orchard' + * to='juliet@capulet.com/balcony'> + * <x xmlns='jabber:x:event'> + * <displayed/> + * <id>message22</id> + * </x> + * </message> + * </pre> + * + */ + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\">"); + // Note: Cancellation events don't specify any tag. They just send the packetID + + // Add the offline tag if the sender requests to be notified of offline events or if + // the target is offline + if (isOffline()) + buf.append("<").append(MessageEvent.OFFLINE).append("/>"); + // Add the delivered tag if the sender requests to be notified when the message is + // delivered or if the target notifies that the message has been delivered + if (isDelivered()) + buf.append("<").append(MessageEvent.DELIVERED).append("/>"); + // Add the displayed tag if the sender requests to be notified when the message is + // displayed or if the target notifies that the message has been displayed + if (isDisplayed()) + buf.append("<").append(MessageEvent.DISPLAYED).append("/>"); + // Add the composing tag if the sender requests to be notified when the target is + // composing a reply or if the target notifies that he/she is composing a reply + if (isComposing()) + buf.append("<").append(MessageEvent.COMPOSING).append("/>"); + // Add the id tag only if the MessageEvent is a notification message (not a request) + if (getPacketID() != null) + buf.append("<id>").append(getPacketID()).append("</id>"); + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + +} diff --git a/src/org/jivesoftware/smackx/packet/MultipleAddresses.java b/src/org/jivesoftware/smackx/packet/MultipleAddresses.java new file mode 100644 index 0000000..522250a --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/MultipleAddresses.java @@ -0,0 +1,205 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Packet extension that contains the list of addresses that a packet should be sent or was sent. + * + * @author Gaston Dombiak + */ +public class MultipleAddresses implements PacketExtension { + + public static final String BCC = "bcc"; + public static final String CC = "cc"; + public static final String NO_REPLY = "noreply"; + public static final String REPLY_ROOM = "replyroom"; + public static final String REPLY_TO = "replyto"; + public static final String TO = "to"; + + + private List<Address> addresses = new ArrayList<Address>(); + + /** + * Adds a new address to which the packet is going to be sent or was sent. + * + * @param type on of the static type (BCC, CC, NO_REPLY, REPLY_ROOM, etc.) + * @param jid the JID address of the recipient. + * @param node used to specify a sub-addressable unit at a particular JID, corresponding to + * a Service Discovery node. + * @param desc used to specify human-readable information for this address. + * @param delivered true when the packet was already delivered to this address. + * @param uri used to specify an external system address, such as a sip:, sips:, or im: URI. + */ + public void addAddress(String type, String jid, String node, String desc, boolean delivered, + String uri) { + // Create a new address with the specificed configuration + Address address = new Address(type); + address.setJid(jid); + address.setNode(node); + address.setDescription(desc); + address.setDelivered(delivered); + address.setUri(uri); + // Add the new address to the list of multiple recipients + addresses.add(address); + } + + /** + * Indicate that the packet being sent should not be replied. + */ + public void setNoReply() { + // Create a new address with the specificed configuration + Address address = new Address(NO_REPLY); + // Add the new address to the list of multiple recipients + addresses.add(address); + } + + /** + * Returns the list of addresses that matches the specified type. Examples of address + * type are: TO, CC, BCC, etc.. + * + * @param type Examples of address type are: TO, CC, BCC, etc. + * @return the list of addresses that matches the specified type. + */ + public List<Address> getAddressesOfType(String type) { + List<Address> answer = new ArrayList<Address>(addresses.size()); + for (Iterator<Address> it = addresses.iterator(); it.hasNext();) { + Address address = (Address) it.next(); + if (address.getType().equals(type)) { + answer.add(address); + } + } + + return answer; + } + + public String getElementName() { + return "addresses"; + } + + public String getNamespace() { + return "http://jabber.org/protocol/address"; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()); + buf.append(" xmlns=\"").append(getNamespace()).append("\">"); + // Loop through all the addresses and append them to the string buffer + for (Iterator<Address> i = addresses.iterator(); i.hasNext();) { + Address address = (Address) i.next(); + buf.append(address.toXML()); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + public static class Address { + + private String type; + private String jid; + private String node; + private String description; + private boolean delivered; + private String uri; + + private Address(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public String getJid() { + return jid; + } + + private void setJid(String jid) { + this.jid = jid; + } + + public String getNode() { + return node; + } + + private void setNode(String node) { + this.node = node; + } + + public String getDescription() { + return description; + } + + private void setDescription(String description) { + this.description = description; + } + + public boolean isDelivered() { + return delivered; + } + + private void setDelivered(boolean delivered) { + this.delivered = delivered; + } + + public String getUri() { + return uri; + } + + private void setUri(String uri) { + this.uri = uri; + } + + private String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<address type=\""); + // Append the address type (e.g. TO/CC/BCC) + buf.append(type).append("\""); + if (jid != null) { + buf.append(" jid=\""); + buf.append(jid).append("\""); + } + if (node != null) { + buf.append(" node=\""); + buf.append(node).append("\""); + } + if (description != null && description.trim().length() > 0) { + buf.append(" desc=\""); + buf.append(description).append("\""); + } + if (delivered) { + buf.append(" delivered=\"true\""); + } + if (uri != null) { + buf.append(" uri=\""); + buf.append(uri).append("\""); + } + buf.append("/>"); + return buf.toString(); + } + } +} diff --git a/src/org/jivesoftware/smackx/packet/Nick.java b/src/org/jivesoftware/smackx/packet/Nick.java new file mode 100644 index 0000000..9a64eaa --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/Nick.java @@ -0,0 +1,112 @@ +/**
+ * $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.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * A minimalistic implementation of a {@link PacketExtension} for nicknames.
+ *
+ * @author Guus der Kinderen, guus.der.kinderen@gmail.com
+ * @see <a href="http://xmpp.org/extensions/xep-0172.html">XEP-0172: User Nickname</a>
+ */
+public class Nick implements PacketExtension {
+
+ public static final String NAMESPACE = "http://jabber.org/protocol/nick";
+
+ public static final String ELEMENT_NAME = "nick";
+
+ private String name = null;
+
+ public Nick(String name) {
+ this.name = name;
+ }
+
+ /**
+ * The value of this nickname
+ *
+ * @return the nickname
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the value of this nickname
+ *
+ * @param name
+ * the name to set
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smack.packet.PacketExtension#getElementName()
+ */
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smack.packet.PacketExtension#getNamespace()
+ */
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smack.packet.PacketExtension#toXML()
+ */
+ public String toXML() {
+ final StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(
+ NAMESPACE).append("\">");
+ buf.append(getName());
+ buf.append("</").append(ELEMENT_NAME).append('>');
+
+ return buf.toString();
+ }
+
+ public static class Provider implements PacketExtensionProvider {
+
+ public PacketExtension parseExtension(XmlPullParser parser)
+ throws Exception {
+
+ parser.next();
+ final String name = parser.getText();
+
+ // Advance to end of extension.
+ while(parser.getEventType() != XmlPullParser.END_TAG) {
+ parser.next();
+ }
+
+ return new Nick(name);
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/packet/OfflineMessageInfo.java b/src/org/jivesoftware/smackx/packet/OfflineMessageInfo.java new file mode 100644 index 0000000..5f9954d --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/OfflineMessageInfo.java @@ -0,0 +1,128 @@ +/** + * $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.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.xmlpull.v1.XmlPullParser; + +/** + * OfflineMessageInfo is an extension included in the retrieved offline messages requested by + * the {@link org.jivesoftware.smackx.OfflineMessageManager}. This extension includes a stamp + * that uniquely identifies the offline message. This stamp may be used for deleting the offline + * message. The stamp may be of the form UTC timestamps but it is not required to have that format. + * + * @author Gaston Dombiak + */ +public class OfflineMessageInfo implements PacketExtension { + + private String node = null; + + /** + * Returns the XML element name of the extension sub-packet root element. + * Always returns "offline" + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return "offline"; + } + + /** + * Returns the XML namespace of the extension sub-packet root element. + * According the specification the namespace is always "http://jabber.org/protocol/offline" + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return "http://jabber.org/protocol/offline"; + } + + /** + * Returns the stamp that uniquely identifies the offline message. This stamp may + * be used for deleting the offline message. The stamp may be of the form UTC timestamps + * but it is not required to have that format. + * + * @return the stamp that uniquely identifies the offline message. + */ + public String getNode() { + return node; + } + + /** + * Sets the stamp that uniquely identifies the offline message. This stamp may + * be used for deleting the offline message. The stamp may be of the form UTC timestamps + * but it is not required to have that format. + * + * @param node the stamp that uniquely identifies the offline message. + */ + public void setNode(String node) { + this.node = node; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\">"); + if (getNode() != null) + buf.append("<item node=\"").append(getNode()).append("\"/>"); + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + public static class Provider implements PacketExtensionProvider { + + /** + * Creates a new Provider. + * ProviderManager requires that every PacketExtensionProvider has a public, + * no-argument constructor + */ + public Provider() { + } + + /** + * Parses a OfflineMessageInfo packet (extension sub-packet). + * + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public PacketExtension parseExtension(XmlPullParser parser) + throws Exception { + OfflineMessageInfo info = new OfflineMessageInfo(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) + info.setNode(parser.getAttributeValue("", "node")); + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("offline")) { + done = true; + } + } + } + + return info; + } + + } +} diff --git a/src/org/jivesoftware/smackx/packet/OfflineMessageRequest.java b/src/org/jivesoftware/smackx/packet/OfflineMessageRequest.java new file mode 100644 index 0000000..1d9d096 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/OfflineMessageRequest.java @@ -0,0 +1,237 @@ +/** + * $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.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.xmlpull.v1.XmlPullParser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Represents a request to get some or all the offline messages of a user. This class can also + * be used for deleting some or all the offline messages of a user. + * + * @author Gaston Dombiak + */ +public class OfflineMessageRequest extends IQ { + + private List<Item> items = new ArrayList<Item>(); + private boolean purge = false; + private boolean fetch = false; + + /** + * Returns an Iterator for item childs that holds information about offline messages to + * view or delete. + * + * @return an Iterator for item childs that holds information about offline messages to + * view or delete. + */ + public Iterator<Item> getItems() { + synchronized (items) { + return Collections.unmodifiableList(new ArrayList<Item>(items)).iterator(); + } + } + + /** + * Adds an item child that holds information about offline messages to view or delete. + * + * @param item the item child that holds information about offline messages to view or delete. + */ + public void addItem(Item item) { + synchronized (items) { + items.add(item); + } + } + + /** + * Returns true if all the offline messages of the user should be deleted. + * + * @return true if all the offline messages of the user should be deleted. + */ + public boolean isPurge() { + return purge; + } + + /** + * Sets if all the offline messages of the user should be deleted. + * + * @param purge true if all the offline messages of the user should be deleted. + */ + public void setPurge(boolean purge) { + this.purge = purge; + } + + /** + * Returns true if all the offline messages of the user should be retrieved. + * + * @return true if all the offline messages of the user should be retrieved. + */ + public boolean isFetch() { + return fetch; + } + + /** + * Sets if all the offline messages of the user should be retrieved. + * + * @param fetch true if all the offline messages of the user should be retrieved. + */ + public void setFetch(boolean fetch) { + this.fetch = fetch; + } + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<offline xmlns=\"http://jabber.org/protocol/offline\">"); + synchronized (items) { + for (int i = 0; i < items.size(); i++) { + Item item = items.get(i); + buf.append(item.toXML()); + } + } + if (purge) { + buf.append("<purge/>"); + } + if (fetch) { + buf.append("<fetch/>"); + } + // Add packet extensions, if any are defined. + buf.append(getExtensionsXML()); + buf.append("</offline>"); + return buf.toString(); + } + + /** + * Item child that holds information about offline messages to view or delete. + * + * @author Gaston Dombiak + */ + public static class Item { + private String action; + private String jid; + private String node; + + /** + * Creates a new item child. + * + * @param node the actor's affiliation to the room + */ + public Item(String node) { + this.node = node; + } + + public String getNode() { + return node; + } + + /** + * Returns "view" or "remove" that indicate if the server should return the specified + * offline message or delete it. + * + * @return "view" or "remove" that indicate if the server should return the specified + * offline message or delete it. + */ + public String getAction() { + return action; + } + + /** + * Sets if the server should return the specified offline message or delete it. Possible + * values are "view" or "remove". + * + * @param action if the server should return the specified offline message or delete it. + */ + public void setAction(String action) { + this.action = action; + } + + public String getJid() { + return jid; + } + + public void setJid(String jid) { + this.jid = jid; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<item"); + if (getAction() != null) { + buf.append(" action=\"").append(getAction()).append("\""); + } + if (getJid() != null) { + buf.append(" jid=\"").append(getJid()).append("\""); + } + if (getNode() != null) { + buf.append(" node=\"").append(getNode()).append("\""); + } + buf.append("/>"); + return buf.toString(); + } + } + + public static class Provider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + OfflineMessageRequest request = new OfflineMessageRequest(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + request.addItem(parseItem(parser)); + } + else if (parser.getName().equals("purge")) { + request.setPurge(true); + } + else if (parser.getName().equals("fetch")) { + request.setFetch(true); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("offline")) { + done = true; + } + } + } + + return request; + } + + private Item parseItem(XmlPullParser parser) throws Exception { + boolean done = false; + Item item = new Item(parser.getAttributeValue("", "node")); + item.setAction(parser.getAttributeValue("", "action")); + item.setJid(parser.getAttributeValue("", "jid")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + done = true; + } + } + } + return item; + } + } +} diff --git a/src/org/jivesoftware/smackx/packet/PEPEvent.java b/src/org/jivesoftware/smackx/packet/PEPEvent.java new file mode 100644 index 0000000..48f1de2 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/PEPEvent.java @@ -0,0 +1,105 @@ +/** + * $RCSfile: PEPEvent.java,v $ + * $Revision: 1.1 $ + * $Date: 2007/11/03 00:14:32 $ + * + * 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.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; + +/** + * Represents XMPP Personal Event Protocol packets.<p> + * + * The 'http://jabber.org/protocol/pubsub#event' namespace is used to publish personal events items from one client + * to subscribed clients (See XEP-163). + * + * @author Jeff Williams + */ +public class PEPEvent implements PacketExtension { + + PEPItem item; + + /** + * Creates a new empty roster exchange package. + * + */ + public PEPEvent() { + super(); + } + + /** + * Creates a new empty roster exchange package. + * + */ + public PEPEvent(PEPItem item) { + super(); + + this.item = item; + } + + public void addPEPItem(PEPItem item) { + this.item = item; + } + + /** + * Returns the XML element name of the extension sub-packet root element. + * Always returns "x" + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return "event"; + } + + /** + * Returns the XML namespace of the extension sub-packet root element. + * According the specification the namespace is always "jabber:x:roster" + * (which is not to be confused with the 'jabber:iq:roster' namespace + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return "http://jabber.org/protocol/pubsub"; + } + + /** + * Returns the XML representation of a Personal Event Publish according the specification. + * + * Usually the XML representation will be inside of a Message XML representation like + * in the following example: + * <pre> + * <message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"> + * <subject>Any subject you want</subject> + * <body>This message contains roster items.</body> + * <x xmlns="jabber:x:roster"> + * <item jid="gato1@gato.home"/> + * <item jid="gato2@gato.home"/> + * </x> + * </message> + * </pre> + * + */ + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\">"); + buf.append(item.toXML()); + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + +} diff --git a/src/org/jivesoftware/smackx/packet/PEPItem.java b/src/org/jivesoftware/smackx/packet/PEPItem.java new file mode 100644 index 0000000..c3ff6f4 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/PEPItem.java @@ -0,0 +1,92 @@ +/** + * $RCSfile: PEPItem.java,v $ + * $Revision: 1.2 $ + * $Date: 2007/11/06 02:05:09 $ + * + * 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.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; + +/** + * Represents XMPP Personal Event Protocol packets.<p> + * + * The 'http://jabber.org/protocol/pubsub#event' namespace is used to publish personal events items from one client + * to subscribed clients (See XEP-163). + * + * @author Jeff Williams + */ +public abstract class PEPItem implements PacketExtension { + + String id; + abstract String getNode(); + abstract String getItemDetailsXML(); + + /** + * Creates a new PEPItem. + * + */ + public PEPItem(String id) { + super(); + this.id = id; + } + + /** + * Returns the XML element name of the extension sub-packet root element. + * Always returns "x" + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return "item"; + } + + /** + * Returns the XML namespace of the extension sub-packet root element. + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return "http://jabber.org/protocol/pubsub"; + } + + /** + * Returns the XML representation of a Personal Event Publish according the specification. + * + * Usually the XML representation will be inside of a Message XML representation like + * in the following example: + * <pre> + * <message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"> + * <subject>Any subject you want</subject> + * <body>This message contains roster items.</body> + * <x xmlns="jabber:x:roster"> + * <item jid="gato1@gato.home"/> + * <item jid="gato2@gato.home"/> + * </x> + * </message> + * </pre> + * + */ + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" id=\"").append(id).append("\">"); + buf.append(getItemDetailsXML()); + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + +} diff --git a/src/org/jivesoftware/smackx/packet/PEPPubSub.java b/src/org/jivesoftware/smackx/packet/PEPPubSub.java new file mode 100644 index 0000000..420ce61 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/PEPPubSub.java @@ -0,0 +1,95 @@ +/** + * $RCSfile: PEPPubSub.java,v $ + * $Revision: 1.2 $ + * $Date: 2007/11/03 04:46:52 $ + * + * 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.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; + +/** + * Represents XMPP PEP/XEP-163 pubsub packets.<p> + * + * The 'http://jabber.org/protocol/pubsub' namespace is used to publish personal events items from one client + * to subscribed clients (See XEP-163). + * + * @author Jeff Williams + */ +public class PEPPubSub extends IQ { + + PEPItem item; + + /** + * Creates a new PubSub. + * + */ + public PEPPubSub(PEPItem item) { + super(); + + this.item = item; + } + + /** + * Returns the XML element name of the extension sub-packet root element. + * Always returns "x" + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return "pubsub"; + } + + /** + * Returns the XML namespace of the extension sub-packet root element. + * According the specification the namespace is always "jabber:x:roster" + * (which is not to be confused with the 'jabber:iq:roster' namespace + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return "http://jabber.org/protocol/pubsub"; + } + + /** + * Returns the XML representation of a Personal Event Publish according the specification. + * + * Usually the XML representation will be inside of a Message XML representation like + * in the following example: + * <pre> + * <message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"> + * <subject>Any subject you want</subject> + * <body>This message contains roster items.</body> + * <x xmlns="jabber:x:roster"> + * <item jid="gato1@gato.home"/> + * <item jid="gato2@gato.home"/> + * </x> + * </message> + * </pre> + * + */ + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\">"); + buf.append("<publish node=\"").append(item.getNode()).append("\">"); + buf.append(item.toXML()); + buf.append("</publish>"); + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + +} diff --git a/src/org/jivesoftware/smackx/packet/PrivateData.java b/src/org/jivesoftware/smackx/packet/PrivateData.java new file mode 100644 index 0000000..3ddb7d5 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/PrivateData.java @@ -0,0 +1,52 @@ +/** + * $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.smackx.packet; + +/** + * Interface to represent private data. Each private data chunk is an XML sub-document + * with a root element name and namespace. + * + * @see org.jivesoftware.smackx.PrivateDataManager + * @author Matt Tucker + */ +public interface PrivateData { + + /** + * Returns the root element name. + * + * @return the element name. + */ + public String getElementName(); + + /** + * Returns the root element XML namespace. + * + * @return the namespace. + */ + public String getNamespace(); + + /** + * Returns the XML reppresentation of the PrivateData. + * + * @return the private data as XML. + */ + public String toXML(); +} diff --git a/src/org/jivesoftware/smackx/packet/RosterExchange.java b/src/org/jivesoftware/smackx/packet/RosterExchange.java new file mode 100644 index 0000000..ad59146 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/RosterExchange.java @@ -0,0 +1,181 @@ +/** + * $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.smackx.packet; + +import org.jivesoftware.smack.Roster; +import org.jivesoftware.smack.RosterEntry; +import org.jivesoftware.smack.RosterGroup; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.RemoteRosterEntry; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Represents XMPP Roster Item Exchange packets.<p> + * + * The 'jabber:x:roster' namespace (which is not to be confused with the 'jabber:iq:roster' + * namespace) is used to send roster items from one client to another. A roster item is sent by + * adding to the <message/> element an <x/> child scoped by the 'jabber:x:roster' namespace. This + * <x/> element may contain one or more <item/> children (one for each roster item to be sent).<p> + * + * Each <item/> element may possess the following attributes:<p> + * + * <jid/> -- The id of the contact being sent. This attribute is required.<br> + * <name/> -- A natural-language nickname for the contact. This attribute is optional.<p> + * + * Each <item/> element may also contain one or more <group/> children specifying the + * natural-language name of a user-specified group, for the purpose of categorizing this contact + * into one or more roster groups. + * + * @author Gaston Dombiak + */ +public class RosterExchange implements PacketExtension { + + private List<RemoteRosterEntry> remoteRosterEntries = new ArrayList<RemoteRosterEntry>(); + + /** + * Creates a new empty roster exchange package. + * + */ + public RosterExchange() { + super(); + } + + /** + * Creates a new roster exchange package with the entries specified in roster. + * + * @param roster the roster to send to other XMPP entity. + */ + public RosterExchange(Roster roster) { + // Add all the roster entries to the new RosterExchange + for (RosterEntry rosterEntry : roster.getEntries()) { + this.addRosterEntry(rosterEntry); + } + } + + /** + * Adds a roster entry to the packet. + * + * @param rosterEntry a roster entry to add. + */ + public void addRosterEntry(RosterEntry rosterEntry) { + // Obtain a String[] from the roster entry groups name + List<String> groupNamesList = new ArrayList<String>(); + String[] groupNames; + for (RosterGroup group : rosterEntry.getGroups()) { + groupNamesList.add(group.getName()); + } + groupNames = groupNamesList.toArray(new String[groupNamesList.size()]); + + // Create a new Entry based on the rosterEntry and add it to the packet + RemoteRosterEntry remoteRosterEntry = new RemoteRosterEntry(rosterEntry.getUser(), + rosterEntry.getName(), groupNames); + + addRosterEntry(remoteRosterEntry); + } + + /** + * Adds a remote roster entry to the packet. + * + * @param remoteRosterEntry a remote roster entry to add. + */ + public void addRosterEntry(RemoteRosterEntry remoteRosterEntry) { + synchronized (remoteRosterEntries) { + remoteRosterEntries.add(remoteRosterEntry); + } + } + + /** + * Returns the XML element name of the extension sub-packet root element. + * Always returns "x" + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return "x"; + } + + /** + * Returns the XML namespace of the extension sub-packet root element. + * According the specification the namespace is always "jabber:x:roster" + * (which is not to be confused with the 'jabber:iq:roster' namespace + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return "jabber:x:roster"; + } + + /** + * Returns an Iterator for the roster entries in the packet. + * + * @return an Iterator for the roster entries in the packet. + */ + public Iterator<RemoteRosterEntry> getRosterEntries() { + synchronized (remoteRosterEntries) { + List<RemoteRosterEntry> entries = Collections.unmodifiableList(new ArrayList<RemoteRosterEntry>(remoteRosterEntries)); + return entries.iterator(); + } + } + + /** + * Returns a count of the entries in the roster exchange. + * + * @return the number of entries in the roster exchange. + */ + public int getEntryCount() { + return remoteRosterEntries.size(); + } + + /** + * Returns the XML representation of a Roster Item Exchange according the specification. + * + * Usually the XML representation will be inside of a Message XML representation like + * in the following example: + * <pre> + * <message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"> + * <subject>Any subject you want</subject> + * <body>This message contains roster items.</body> + * <x xmlns="jabber:x:roster"> + * <item jid="gato1@gato.home"/> + * <item jid="gato2@gato.home"/> + * </x> + * </message> + * </pre> + * + */ + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\">"); + // Loop through all roster entries and append them to the string buffer + for (Iterator<RemoteRosterEntry> i = getRosterEntries(); i.hasNext();) { + RemoteRosterEntry remoteRosterEntry = i.next(); + buf.append(remoteRosterEntry.toXML()); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + +} diff --git a/src/org/jivesoftware/smackx/packet/SharedGroupsInfo.java b/src/org/jivesoftware/smackx/packet/SharedGroupsInfo.java new file mode 100644 index 0000000..59bd98e --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/SharedGroupsInfo.java @@ -0,0 +1,92 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * IQ packet used for discovering the user's shared groups and for getting the answer back
+ * from the server.<p>
+ *
+ * Important note: This functionality is not part of the XMPP spec and it will only work
+ * with Wildfire.
+ *
+ * @author Gaston Dombiak
+ */
+public class SharedGroupsInfo extends IQ {
+
+ private List<String> groups = new ArrayList<String>();
+
+ /**
+ * Returns a collection with the shared group names returned from the server.
+ *
+ * @return collection with the shared group names returned from the server.
+ */
+ public List<String> getGroups() {
+ return groups;
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<sharedgroup xmlns=\"http://www.jivesoftware.org/protocol/sharedgroup\">");
+ for (Iterator<String> it=groups.iterator(); it.hasNext();) {
+ buf.append("<group>").append(it.next()).append("</group>");
+ }
+ buf.append("</sharedgroup>");
+ return buf.toString();
+ }
+
+ /**
+ * Internal Search service Provider.
+ */
+ public static class Provider implements IQProvider {
+
+ /**
+ * Provider Constructor.
+ */
+ public Provider() {
+ super();
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ SharedGroupsInfo groupsInfo = new SharedGroupsInfo();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG && parser.getName().equals("group")) {
+ groupsInfo.getGroups().add(parser.nextText());
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("sharedgroup")) {
+ done = true;
+ }
+ }
+ }
+ return groupsInfo;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/packet/StreamInitiation.java b/src/org/jivesoftware/smackx/packet/StreamInitiation.java new file mode 100644 index 0000000..511a02c --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/StreamInitiation.java @@ -0,0 +1,419 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.packet;
+
+import java.util.Date;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * The process by which two entities initiate a stream.
+ *
+ * @author Alexander Wenckus
+ */
+public class StreamInitiation extends IQ {
+
+ private String id;
+
+ private String mimeType;
+
+ private File file;
+
+ private Feature featureNegotiation;
+
+ /**
+ * The "id" attribute is an opaque identifier. This attribute MUST be
+ * present on type='set', and MUST be a valid string. This SHOULD NOT be
+ * sent back on type='result', since the <iq/> "id" attribute provides the
+ * only context needed. This value is generated by the Sender, and the same
+ * value MUST be used throughout a session when talking to the Receiver.
+ *
+ * @param id The "id" attribute.
+ */
+ public void setSesssionID(final String id) {
+ this.id = id;
+ }
+
+ /**
+ * Uniquely identifies a stream initiation to the recipient.
+ *
+ * @return The "id" attribute.
+ * @see #setSesssionID(String)
+ */
+ public String getSessionID() {
+ return id;
+ }
+
+ /**
+ * The "mime-type" attribute identifies the MIME-type for the data across
+ * the stream. This attribute MUST be a valid MIME-type as registered with
+ * the Internet Assigned Numbers Authority (IANA) [3] (specifically, as
+ * listed at <http://www.iana.org/assignments/media-types>). During
+ * negotiation, this attribute SHOULD be present, and is otherwise not
+ * required. If not included during negotiation, its value is assumed to be
+ * "binary/octect-stream".
+ *
+ * @param mimeType The valid mime-type.
+ */
+ public void setMimeType(final String mimeType) {
+ this.mimeType = mimeType;
+ }
+
+ /**
+ * Identifies the type of file that is desired to be transfered.
+ *
+ * @return The mime-type.
+ * @see #setMimeType(String)
+ */
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ /**
+ * Sets the file which contains the information pertaining to the file to be
+ * transfered.
+ *
+ * @param file The file identified by the stream initiator to be sent.
+ */
+ public void setFile(final File file) {
+ this.file = file;
+ }
+
+ /**
+ * Returns the file containing the information about the request.
+ *
+ * @return Returns the file containing the information about the request.
+ */
+ public File getFile() {
+ return file;
+ }
+
+ /**
+ * Sets the data form which contains the valid methods of stream neotiation
+ * and transfer.
+ *
+ * @param form The dataform containing the methods.
+ */
+ public void setFeatureNegotiationForm(final DataForm form) {
+ this.featureNegotiation = new Feature(form);
+ }
+
+ /**
+ * Returns the data form which contains the valid methods of stream
+ * neotiation and transfer.
+ *
+ * @return Returns the data form which contains the valid methods of stream
+ * neotiation and transfer.
+ */
+ public DataForm getFeatureNegotiationForm() {
+ return featureNegotiation.getData();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.jivesoftware.smack.packet.IQ#getChildElementXML()
+ */
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ if (this.getType().equals(IQ.Type.SET)) {
+ buf.append("<si xmlns=\"http://jabber.org/protocol/si\" ");
+ if (getSessionID() != null) {
+ buf.append("id=\"").append(getSessionID()).append("\" ");
+ }
+ if (getMimeType() != null) {
+ buf.append("mime-type=\"").append(getMimeType()).append("\" ");
+ }
+ buf
+ .append("profile=\"http://jabber.org/protocol/si/profile/file-transfer\">");
+
+ // Add the file section if there is one.
+ String fileXML = file.toXML();
+ if (fileXML != null) {
+ buf.append(fileXML);
+ }
+ }
+ else if (this.getType().equals(IQ.Type.RESULT)) {
+ buf.append("<si xmlns=\"http://jabber.org/protocol/si\">");
+ }
+ else {
+ throw new IllegalArgumentException("IQ Type not understood");
+ }
+ if (featureNegotiation != null) {
+ buf.append(featureNegotiation.toXML());
+ }
+ buf.append("</si>");
+ return buf.toString();
+ }
+
+ /**
+ * <ul>
+ * <li>size: The size, in bytes, of the data to be sent.</li>
+ * <li>name: The name of the file that the Sender wishes to send.</li>
+ * <li>date: The last modification time of the file. This is specified
+ * using the DateTime profile as described in Jabber Date and Time Profiles.</li>
+ * <li>hash: The MD5 sum of the file contents.</li>
+ * </ul>
+ * <p/>
+ * <p/>
+ * <desc> is used to provide a sender-generated description of the
+ * file so the receiver can better understand what is being sent. It MUST
+ * NOT be sent in the result.
+ * <p/>
+ * <p/>
+ * When <range> is sent in the offer, it should have no attributes.
+ * This signifies that the sender can do ranged transfers. When a Stream
+ * Initiation result is sent with the <range> element, it uses these
+ * attributes:
+ * <p/>
+ * <ul>
+ * <li>offset: Specifies the position, in bytes, to start transferring the
+ * file data from. This defaults to zero (0) if not specified.</li>
+ * <li>length - Specifies the number of bytes to retrieve starting at
+ * offset. This defaults to the length of the file from offset to the end.</li>
+ * </ul>
+ * <p/>
+ * <p/>
+ * Both attributes are OPTIONAL on the <range> element. Sending no
+ * attributes is synonymous with not sending the <range> element. When
+ * no <range> element is sent in the Stream Initiation result, the
+ * Sender MUST send the complete file starting at offset 0. More generally,
+ * data is sent over the stream byte for byte starting at the offset
+ * position for the length specified.
+ *
+ * @author Alexander Wenckus
+ */
+ public static class File implements PacketExtension {
+
+ private final String name;
+
+ private final long size;
+
+ private String hash;
+
+ private Date date;
+
+ private String desc;
+
+ private boolean isRanged;
+
+ /**
+ * Constructor providing the name of the file and its size.
+ *
+ * @param name The name of the file.
+ * @param size The size of the file in bytes.
+ */
+ public File(final String name, final long size) {
+ if (name == null) {
+ throw new NullPointerException("name cannot be null");
+ }
+
+ this.name = name;
+ this.size = size;
+ }
+
+ /**
+ * Returns the file's name.
+ *
+ * @return Returns the file's name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the file's size.
+ *
+ * @return Returns the file's size.
+ */
+ public long getSize() {
+ return size;
+ }
+
+ /**
+ * Sets the MD5 sum of the file's contents
+ *
+ * @param hash The MD5 sum of the file's contents.
+ */
+ public void setHash(final String hash) {
+ this.hash = hash;
+ }
+
+ /**
+ * Returns the MD5 sum of the file's contents
+ *
+ * @return Returns the MD5 sum of the file's contents
+ */
+ public String getHash() {
+ return hash;
+ }
+
+ /**
+ * Sets the date that the file was last modified.
+ *
+ * @param date The date that the file was last modified.
+ */
+ public void setDate(Date date) {
+ this.date = date;
+ }
+
+ /**
+ * Returns the date that the file was last modified.
+ *
+ * @return Returns the date that the file was last modified.
+ */
+ public Date getDate() {
+ return date;
+ }
+
+ /**
+ * Sets the description of the file.
+ *
+ * @param desc The description of the file so that the file reciever can
+ * know what file it is.
+ */
+ public void setDesc(final String desc) {
+ this.desc = desc;
+ }
+
+ /**
+ * Returns the description of the file.
+ *
+ * @return Returns the description of the file.
+ */
+ public String getDesc() {
+ return desc;
+ }
+
+ /**
+ * True if a range can be provided and false if it cannot.
+ *
+ * @param isRanged True if a range can be provided and false if it cannot.
+ */
+ public void setRanged(final boolean isRanged) {
+ this.isRanged = isRanged;
+ }
+
+ /**
+ * Returns whether or not the initiator can support a range for the file
+ * tranfer.
+ *
+ * @return Returns whether or not the initiator can support a range for
+ * the file tranfer.
+ */
+ public boolean isRanged() {
+ return isRanged;
+ }
+
+ public String getElementName() {
+ return "file";
+ }
+
+ public String getNamespace() {
+ return "http://jabber.org/protocol/si/profile/file-transfer";
+ }
+
+ public String toXML() {
+ StringBuilder buffer = new StringBuilder();
+
+ buffer.append("<").append(getElementName()).append(" xmlns=\"")
+ .append(getNamespace()).append("\" ");
+
+ if (getName() != null) {
+ buffer.append("name=\"").append(StringUtils.escapeForXML(getName())).append("\" ");
+ }
+
+ if (getSize() > 0) {
+ buffer.append("size=\"").append(getSize()).append("\" ");
+ }
+
+ if (getDate() != null) {
+ buffer.append("date=\"").append(StringUtils.formatXEP0082Date(date)).append("\" ");
+ } + + if (getHash() != null) { + buffer.append("hash=\"").append(getHash()).append("\" "); + } + + if ((desc != null && desc.length() > 0) || isRanged) { + buffer.append(">"); + if (getDesc() != null && desc.length() > 0) { + buffer.append("<desc>").append(StringUtils.escapeForXML(getDesc())).append("</desc>"); + } + if (isRanged()) { + buffer.append("<range/>"); + } + buffer.append("</").append(getElementName()).append(">"); + } + else { + buffer.append("/>"); + } + return buffer.toString(); + } + } + + /** + * The feature negotiation portion of the StreamInitiation packet. + * + * @author Alexander Wenckus + * + */ + public class Feature implements PacketExtension { + + private final DataForm data; + + /** + * The dataform can be provided as part of the constructor. + * + * @param data The dataform. + */ + public Feature(final DataForm data) { + this.data = data; + } + + /** + * Returns the dataform associated with the feature negotiation. + * + * @return Returns the dataform associated with the feature negotiation. + */ + public DataForm getData() { + return data; + } + + public String getNamespace() { + return "http://jabber.org/protocol/feature-neg"; + } + + public String getElementName() { + return "feature"; + } + + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf + .append("<feature xmlns=\"http://jabber.org/protocol/feature-neg\">"); + buf.append(data.toXML()); + buf.append("</feature>"); + return buf.toString(); + } + } +} diff --git a/src/org/jivesoftware/smackx/packet/Time.java b/src/org/jivesoftware/smackx/packet/Time.java new file mode 100644 index 0000000..5294e77 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/Time.java @@ -0,0 +1,198 @@ +/** + * $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.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +/** + * A Time IQ packet, which is used by XMPP clients to exchange their respective local + * times. Clients that wish to fully support the entitity time protocol should register + * a PacketListener for incoming time requests that then respond with the local time. + * This class can be used to request the time from other clients, such as in the + * following code snippet: + * + * <pre> + * // Request the time from a remote user. + * Time timeRequest = new Time(); + * timeRequest.setType(IQ.Type.GET); + * timeRequest.setTo(someUser@example.com/resource); + * + * // Create a packet collector to listen for a response. + * PacketCollector collector = con.createPacketCollector( + * new PacketIDFilter(timeRequest.getPacketID())); + * + * con.sendPacket(timeRequest); + * + * // Wait up to 5 seconds for a result. + * IQ result = (IQ)collector.nextResult(5000); + * if (result != null && result.getType() == IQ.Type.RESULT) { + * Time timeResult = (Time)result; + * // Do something with result... + * }</pre><p> + * + * Warning: this is an non-standard protocol documented by + * <a href="http://www.xmpp.org/extensions/xep-0090.html">XEP-0090</a>. Because this is a + * non-standard protocol, it is subject to change. + * + * @author Matt Tucker + */ +public class Time extends IQ { + + private static SimpleDateFormat utcFormat = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"); + private static DateFormat displayFormat = DateFormat.getDateTimeInstance(); + + private String utc = null; + private String tz = null; + private String display = null; + + /** + * Creates a new Time instance with empty values for all fields. + */ + public Time() { + + } + + /** + * Creates a new Time instance using the specified calendar instance as + * the time value to send. + * + * @param cal the time value. + */ + public Time(Calendar cal) { + TimeZone timeZone = cal.getTimeZone(); + tz = cal.getTimeZone().getID(); + display = displayFormat.format(cal.getTime()); + // Convert local time to the UTC time. + utc = utcFormat.format(new Date( + cal.getTimeInMillis() - timeZone.getOffset(cal.getTimeInMillis()))); + } + + /** + * Returns the local time or <tt>null</tt> if the time hasn't been set. + * + * @return the lcocal time. + */ + public Date getTime() { + if (utc == null) { + return null; + } + Date date = null; + try { + Calendar cal = Calendar.getInstance(); + // Convert the UTC time to local time. + cal.setTime(new Date(utcFormat.parse(utc).getTime() + + cal.getTimeZone().getOffset(cal.getTimeInMillis()))); + date = cal.getTime(); + } + catch (Exception e) { + e.printStackTrace(); + } + return date; + } + + /** + * Sets the time using the local time. + * + * @param time the current local time. + */ + public void setTime(Date time) { + // Convert local time to UTC time. + utc = utcFormat.format(new Date( + time.getTime() - TimeZone.getDefault().getOffset(time.getTime()))); + } + + /** + * Returns the time as a UTC formatted String using the format CCYYMMDDThh:mm:ss. + * + * @return the time as a UTC formatted String. + */ + public String getUtc() { + return utc; + } + + /** + * Sets the time using UTC formatted String in the format CCYYMMDDThh:mm:ss. + * + * @param utc the time using a formatted String. + */ + public void setUtc(String utc) { + this.utc = utc; + + } + + /** + * Returns the time zone. + * + * @return the time zone. + */ + public String getTz() { + return tz; + } + + /** + * Sets the time zone. + * + * @param tz the time zone. + */ + public void setTz(String tz) { + this.tz = tz; + } + + /** + * Returns the local (non-utc) time in human-friendly format. + * + * @return the local time in human-friendly format. + */ + public String getDisplay() { + return display; + } + + /** + * Sets the local time in human-friendly format. + * + * @param display the local time in human-friendly format. + */ + public void setDisplay(String display) { + this.display = display; + } + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<query xmlns=\"jabber:iq:time\">"); + if (utc != null) { + buf.append("<utc>").append(utc).append("</utc>"); + } + if (tz != null) { + buf.append("<tz>").append(tz).append("</tz>"); + } + if (display != null) { + buf.append("<display>").append(display).append("</display>"); + } + buf.append("</query>"); + return buf.toString(); + } +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/packet/VCard.java b/src/org/jivesoftware/smackx/packet/VCard.java new file mode 100644 index 0000000..9766db8 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/VCard.java @@ -0,0 +1,883 @@ +/** + * $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.smackx.packet; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.util.StringUtils; + +/** + * A VCard class for use with the + * <a href="http://www.jivesoftware.org/smack/" target="_blank">SMACK jabber library</a>.<p> + * <p/> + * You should refer to the + * <a href="http://www.jabber.org/jeps/jep-0054.html" target="_blank">JEP-54 documentation</a>.<p> + * <p/> + * Please note that this class is incomplete but it does provide the most commonly found + * information in vCards. Also remember that VCard transfer is not a standard, and the protocol + * may change or be replaced.<p> + * <p/> + * <b>Usage:</b> + * <pre> + * <p/> + * // To save VCard: + * <p/> + * VCard vCard = new VCard(); + * vCard.setFirstName("kir"); + * vCard.setLastName("max"); + * vCard.setEmailHome("foo@fee.bar"); + * vCard.setJabberId("jabber@id.org"); + * vCard.setOrganization("Jetbrains, s.r.o"); + * vCard.setNickName("KIR"); + * <p/> + * vCard.setField("TITLE", "Mr"); + * vCard.setAddressFieldHome("STREET", "Some street"); + * vCard.setAddressFieldWork("CTRY", "US"); + * vCard.setPhoneWork("FAX", "3443233"); + * <p/> + * vCard.save(connection); + * <p/> + * // To load VCard: + * <p/> + * VCard vCard = new VCard(); + * vCard.load(conn); // load own VCard + * vCard.load(conn, "joe@foo.bar"); // load someone's VCard + * </pre> + * + * @author Kirill Maximov (kir@maxkir.com) + */ +public class VCard extends IQ { + + /** + * Phone types: + * VOICE?, FAX?, PAGER?, MSG?, CELL?, VIDEO?, BBS?, MODEM?, ISDN?, PCS?, PREF? + */ + private Map<String, String> homePhones = new HashMap<String, String>(); + private Map<String, String> workPhones = new HashMap<String, String>(); + + + /** + * Address types: + * POSTAL?, PARCEL?, (DOM | INTL)?, PREF?, POBOX?, EXTADR?, STREET?, LOCALITY?, + * REGION?, PCODE?, CTRY? + */ + private Map<String, String> homeAddr = new HashMap<String, String>(); + private Map<String, String> workAddr = new HashMap<String, String>(); + + private String firstName; + private String lastName; + private String middleName; + + private String emailHome; + private String emailWork; + + private String organization; + private String organizationUnit; + + private String photoMimeType; + private String photoBinval; + + /** + * Such as DESC ROLE GEO etc.. see JEP-0054 + */ + private Map<String, String> otherSimpleFields = new HashMap<String, String>(); + + // fields that, as they are should not be escaped before forwarding to the server + private Map<String, String> otherUnescapableFields = new HashMap<String, String>(); + + public VCard() { + } + + /** + * Set generic VCard field. + * + * @param field value of field. Possible values: NICKNAME, PHOTO, BDAY, JABBERID, MAILER, TZ, + * GEO, TITLE, ROLE, LOGO, NOTE, PRODID, REV, SORT-STRING, SOUND, UID, URL, DESC. + */ + public String getField(String field) { + return otherSimpleFields.get(field); + } + + /** + * Set generic VCard field. + * + * @param value value of field + * @param field field to set. See {@link #getField(String)} + * @see #getField(String) + */ + public void setField(String field, String value) { + setField(field, value, false); + } + + /** + * Set generic, unescapable VCard field. If unescabale is set to true, XML maybe a part of the + * value. + * + * @param value value of field + * @param field field to set. See {@link #getField(String)} + * @param isUnescapable True if the value should not be escaped, and false if it should. + */ + public void setField(String field, String value, boolean isUnescapable) { + if (!isUnescapable) { + otherSimpleFields.put(field, value); + } + else { + otherUnescapableFields.put(field, value); + } + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + // Update FN field + updateFN(); + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + // Update FN field + updateFN(); + } + + public String getMiddleName() { + return middleName; + } + + public void setMiddleName(String middleName) { + this.middleName = middleName; + // Update FN field + updateFN(); + } + + public String getNickName() { + return otherSimpleFields.get("NICKNAME"); + } + + public void setNickName(String nickName) { + otherSimpleFields.put("NICKNAME", nickName); + } + + public String getEmailHome() { + return emailHome; + } + + public void setEmailHome(String email) { + this.emailHome = email; + } + + public String getEmailWork() { + return emailWork; + } + + public void setEmailWork(String emailWork) { + this.emailWork = emailWork; + } + + public String getJabberId() { + return otherSimpleFields.get("JABBERID"); + } + + public void setJabberId(String jabberId) { + otherSimpleFields.put("JABBERID", jabberId); + } + + public String getOrganization() { + return organization; + } + + public void setOrganization(String organization) { + this.organization = organization; + } + + public String getOrganizationUnit() { + return organizationUnit; + } + + public void setOrganizationUnit(String organizationUnit) { + this.organizationUnit = organizationUnit; + } + + /** + * Get home address field + * + * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET, + * LOCALITY, REGION, PCODE, CTRY + */ + public String getAddressFieldHome(String addrField) { + return homeAddr.get(addrField); + } + + /** + * Set home address field + * + * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET, + * LOCALITY, REGION, PCODE, CTRY + */ + public void setAddressFieldHome(String addrField, String value) { + homeAddr.put(addrField, value); + } + + /** + * Get work address field + * + * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET, + * LOCALITY, REGION, PCODE, CTRY + */ + public String getAddressFieldWork(String addrField) { + return workAddr.get(addrField); + } + + /** + * Set work address field + * + * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET, + * LOCALITY, REGION, PCODE, CTRY + */ + public void setAddressFieldWork(String addrField, String value) { + workAddr.put(addrField, value); + } + + + /** + * Set home phone number + * + * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF + * @param phoneNum phone number + */ + public void setPhoneHome(String phoneType, String phoneNum) { + homePhones.put(phoneType, phoneNum); + } + + /** + * Get home phone number + * + * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF + */ + public String getPhoneHome(String phoneType) { + return homePhones.get(phoneType); + } + + /** + * Set work phone number + * + * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF + * @param phoneNum phone number + */ + public void setPhoneWork(String phoneType, String phoneNum) { + workPhones.put(phoneType, phoneNum); + } + + /** + * Get work phone number + * + * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF + */ + public String getPhoneWork(String phoneType) { + return workPhones.get(phoneType); + } + + /** + * Set the avatar for the VCard by specifying the url to the image. + * + * @param avatarURL the url to the image(png,jpeg,gif,bmp) + */ + public void setAvatar(URL avatarURL) { + byte[] bytes = new byte[0]; + try { + bytes = getBytes(avatarURL); + } + catch (IOException e) { + e.printStackTrace(); + } + + setAvatar(bytes); + } + + /** + * Removes the avatar from the vCard + * + * This is done by setting the PHOTO value to the empty string as defined in XEP-0153 + */ + public void removeAvatar() { + // Remove avatar (if any) + photoBinval = null; + photoMimeType = null; + } + + /** + * Specify the bytes of the JPEG for the avatar to use. + * If bytes is null, then the avatar will be removed. + * 'image/jpeg' will be used as MIME type. + * + * @param bytes the bytes of the avatar, or null to remove the avatar data + */ + public void setAvatar(byte[] bytes) { + setAvatar(bytes, "image/jpeg"); + } + + /** + * Specify the bytes for the avatar to use as well as the mime type. + * + * @param bytes the bytes of the avatar. + * @param mimeType the mime type of the avatar. + */ + public void setAvatar(byte[] bytes, String mimeType) { + // If bytes is null, remove the avatar + if (bytes == null) { + removeAvatar(); + return; + } + + // Otherwise, add to mappings. + String encodedImage = StringUtils.encodeBase64(bytes); + + setAvatar(encodedImage, mimeType); + } + + /** + * Specify the Avatar used for this vCard. + * + * @param encodedImage the Base64 encoded image as String + * @param mimeType the MIME type of the image + */ + public void setAvatar(String encodedImage, String mimeType) { + photoBinval = encodedImage; + photoMimeType = mimeType; + } + + /** + * Return the byte representation of the avatar(if one exists), otherwise returns null if + * no avatar could be found. + * <b>Example 1</b> + * <pre> + * // Load Avatar from VCard + * byte[] avatarBytes = vCard.getAvatar(); + * <p/> + * // To create an ImageIcon for Swing applications + * ImageIcon icon = new ImageIcon(avatar); + * <p/> + * // To create just an image object from the bytes + * ByteArrayInputStream bais = new ByteArrayInputStream(avatar); + * try { + * Image image = ImageIO.read(bais); + * } + * catch (IOException e) { + * e.printStackTrace(); + * } + * </pre> + * + * @return byte representation of avatar. + */ + public byte[] getAvatar() { + if (photoBinval == null) { + return null; + } + return StringUtils.decodeBase64(photoBinval); + } + + /** + * Returns the MIME Type of the avatar or null if none is set + * + * @return the MIME Type of the avatar or null + */ + public String getAvatarMimeType() { + return photoMimeType; + } + + /** + * Common code for getting the bytes of a url. + * + * @param url the url to read. + */ + public static byte[] getBytes(URL url) throws IOException { + final String path = url.getPath(); + final File file = new File(path); + if (file.exists()) { + return getFileBytes(file); + } + + return null; + } + + private static byte[] getFileBytes(File file) throws IOException { + BufferedInputStream bis = null; + try { + bis = new BufferedInputStream(new FileInputStream(file)); + int bytes = (int) file.length(); + byte[] buffer = new byte[bytes]; + int readBytes = bis.read(buffer); + if (readBytes != buffer.length) { + throw new IOException("Entire file not read"); + } + return buffer; + } + finally { + if (bis != null) { + bis.close(); + } + } + } + + /** + * Returns the SHA-1 Hash of the Avatar image. + * + * @return the SHA-1 Hash of the Avatar image. + */ + public String getAvatarHash() { + byte[] bytes = getAvatar(); + if (bytes == null) { + return null; + } + + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-1"); + } + catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + return null; + } + + digest.update(bytes); + return StringUtils.encodeHex(digest.digest()); + } + + private void updateFN() { + StringBuilder sb = new StringBuilder(); + if (firstName != null) { + sb.append(StringUtils.escapeForXML(firstName)).append(' '); + } + if (middleName != null) { + sb.append(StringUtils.escapeForXML(middleName)).append(' '); + } + if (lastName != null) { + sb.append(StringUtils.escapeForXML(lastName)); + } + setField("FN", sb.toString()); + } + + /** + * Save this vCard for the user connected by 'connection'. Connection should be authenticated + * and not anonymous.<p> + * <p/> + * NOTE: the method is asynchronous and does not wait for the returned value. + * + * @param connection the Connection to use. + * @throws XMPPException thrown if there was an issue setting the VCard in the server. + */ + public void save(Connection connection) throws XMPPException { + checkAuthenticated(connection, true); + + setType(IQ.Type.SET); + setFrom(connection.getUser()); + PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(getPacketID())); + connection.sendPacket(this); + + Packet response = collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Cancel the collector. + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from server on status set."); + } + if (response.getError() != null) { + throw new XMPPException(response.getError()); + } + } + + /** + * Load VCard information for a connected user. Connection should be authenticated + * and not anonymous. + */ + public void load(Connection connection) throws XMPPException { + checkAuthenticated(connection, true); + + setFrom(connection.getUser()); + doLoad(connection, connection.getUser()); + } + + /** + * Load VCard information for a given user. Connection should be authenticated and not anonymous. + */ + public void load(Connection connection, String user) throws XMPPException { + checkAuthenticated(connection, false); + + setTo(user); + doLoad(connection, user); + } + + private void doLoad(Connection connection, String user) throws XMPPException { + setType(Type.GET); + PacketCollector collector = connection.createPacketCollector( + new PacketIDFilter(getPacketID())); + connection.sendPacket(this); + + VCard result = null; + try { + result = (VCard) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + if (result == null) { + String errorMessage = "Timeout getting VCard information"; + throw new XMPPException(errorMessage, new XMPPError( + XMPPError.Condition.request_timeout, errorMessage)); + } + if (result.getError() != null) { + throw new XMPPException(result.getError()); + } + } + catch (ClassCastException e) { + System.out.println("No VCard for " + user); + } + + copyFieldsFrom(result); + } + + public String getChildElementXML() { + StringBuilder sb = new StringBuilder(); + new VCardWriter(sb).write(); + return sb.toString(); + } + + private void copyFieldsFrom(VCard from) { + Field[] fields = VCard.class.getDeclaredFields(); + for (Field field : fields) { + if (field.getDeclaringClass() == VCard.class && + !Modifier.isFinal(field.getModifiers())) { + try { + field.setAccessible(true); + field.set(this, field.get(from)); + } + catch (IllegalAccessException e) { + throw new RuntimeException("This cannot happen:" + field, e); + } + } + } + } + + private void checkAuthenticated(Connection connection, boolean checkForAnonymous) { + if (connection == null) { + throw new IllegalArgumentException("No connection was provided"); + } + if (!connection.isAuthenticated()) { + throw new IllegalArgumentException("Connection is not authenticated"); + } + if (checkForAnonymous && connection.isAnonymous()) { + throw new IllegalArgumentException("Connection cannot be anonymous"); + } + } + + private boolean hasContent() { + //noinspection OverlyComplexBooleanExpression + return hasNameField() + || hasOrganizationFields() + || emailHome != null + || emailWork != null + || otherSimpleFields.size() > 0 + || otherUnescapableFields.size() > 0 + || homeAddr.size() > 0 + || homePhones.size() > 0 + || workAddr.size() > 0 + || workPhones.size() > 0 + || photoBinval != null + ; + } + + private boolean hasNameField() { + return firstName != null || lastName != null || middleName != null; + } + + private boolean hasOrganizationFields() { + return organization != null || organizationUnit != null; + } + + // Used in tests: + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final VCard vCard = (VCard) o; + + if (emailHome != null ? !emailHome.equals(vCard.emailHome) : vCard.emailHome != null) { + return false; + } + if (emailWork != null ? !emailWork.equals(vCard.emailWork) : vCard.emailWork != null) { + return false; + } + if (firstName != null ? !firstName.equals(vCard.firstName) : vCard.firstName != null) { + return false; + } + if (!homeAddr.equals(vCard.homeAddr)) { + return false; + } + if (!homePhones.equals(vCard.homePhones)) { + return false; + } + if (lastName != null ? !lastName.equals(vCard.lastName) : vCard.lastName != null) { + return false; + } + if (middleName != null ? !middleName.equals(vCard.middleName) : vCard.middleName != null) { + return false; + } + if (organization != null ? + !organization.equals(vCard.organization) : vCard.organization != null) { + return false; + } + if (organizationUnit != null ? + !organizationUnit.equals(vCard.organizationUnit) : vCard.organizationUnit != null) { + return false; + } + if (!otherSimpleFields.equals(vCard.otherSimpleFields)) { + return false; + } + if (!workAddr.equals(vCard.workAddr)) { + return false; + } + if (photoBinval != null ? !photoBinval.equals(vCard.photoBinval) : vCard.photoBinval != null) { + return false; + } + + return workPhones.equals(vCard.workPhones); + } + + public int hashCode() { + int result; + result = homePhones.hashCode(); + result = 29 * result + workPhones.hashCode(); + result = 29 * result + homeAddr.hashCode(); + result = 29 * result + workAddr.hashCode(); + result = 29 * result + (firstName != null ? firstName.hashCode() : 0); + result = 29 * result + (lastName != null ? lastName.hashCode() : 0); + result = 29 * result + (middleName != null ? middleName.hashCode() : 0); + result = 29 * result + (emailHome != null ? emailHome.hashCode() : 0); + result = 29 * result + (emailWork != null ? emailWork.hashCode() : 0); + result = 29 * result + (organization != null ? organization.hashCode() : 0); + result = 29 * result + (organizationUnit != null ? organizationUnit.hashCode() : 0); + result = 29 * result + otherSimpleFields.hashCode(); + result = 29 * result + (photoBinval != null ? photoBinval.hashCode() : 0); + return result; + } + + public String toString() { + return getChildElementXML(); + } + + //============================================================== + + private class VCardWriter { + + private final StringBuilder sb; + + VCardWriter(StringBuilder sb) { + this.sb = sb; + } + + public void write() { + appendTag("vCard", "xmlns", "vcard-temp", hasContent(), new ContentBuilder() { + public void addTagContent() { + buildActualContent(); + } + }); + } + + private void buildActualContent() { + if (hasNameField()) { + appendN(); + } + + appendOrganization(); + appendGenericFields(); + appendPhoto(); + + appendEmail(emailWork, "WORK"); + appendEmail(emailHome, "HOME"); + + appendPhones(workPhones, "WORK"); + appendPhones(homePhones, "HOME"); + + appendAddress(workAddr, "WORK"); + appendAddress(homeAddr, "HOME"); + } + + private void appendPhoto() { + if (photoBinval == null) + return; + + appendTag("PHOTO", true, new ContentBuilder() { + public void addTagContent() { + appendTag("BINVAL", photoBinval); // No need to escape photoBinval, as it's already Base64 encoded + appendTag("TYPE", StringUtils.escapeForXML(photoMimeType)); + } + }); + } + private void appendEmail(final String email, final String type) { + if (email != null) { + appendTag("EMAIL", true, new ContentBuilder() { + public void addTagContent() { + appendEmptyTag(type); + appendEmptyTag("INTERNET"); + appendEmptyTag("PREF"); + appendTag("USERID", StringUtils.escapeForXML(email)); + } + }); + } + } + + private void appendPhones(Map<String, String> phones, final String code) { + Iterator<Map.Entry<String, String>> it = phones.entrySet().iterator(); + while (it.hasNext()) { + final Map.Entry<String,String> entry = it.next(); + appendTag("TEL", true, new ContentBuilder() { + public void addTagContent() { + appendEmptyTag(entry.getKey()); + appendEmptyTag(code); + appendTag("NUMBER", StringUtils.escapeForXML(entry.getValue())); + } + }); + } + } + + private void appendAddress(final Map<String, String> addr, final String code) { + if (addr.size() > 0) { + appendTag("ADR", true, new ContentBuilder() { + public void addTagContent() { + appendEmptyTag(code); + + Iterator<Map.Entry<String, String>> it = addr.entrySet().iterator(); + while (it.hasNext()) { + final Entry<String, String> entry = it.next(); + appendTag(entry.getKey(), StringUtils.escapeForXML(entry.getValue())); + } + } + }); + } + } + + private void appendEmptyTag(Object tag) { + sb.append('<').append(tag).append("/>"); + } + + private void appendGenericFields() { + Iterator<Map.Entry<String, String>> it = otherSimpleFields.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry<String, String> entry = it.next(); + appendTag(entry.getKey().toString(), + StringUtils.escapeForXML(entry.getValue())); + } + + it = otherUnescapableFields.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry<String, String> entry = it.next(); + appendTag(entry.getKey().toString(),entry.getValue()); + } + } + + private void appendOrganization() { + if (hasOrganizationFields()) { + appendTag("ORG", true, new ContentBuilder() { + public void addTagContent() { + appendTag("ORGNAME", StringUtils.escapeForXML(organization)); + appendTag("ORGUNIT", StringUtils.escapeForXML(organizationUnit)); + } + }); + } + } + + private void appendN() { + appendTag("N", true, new ContentBuilder() { + public void addTagContent() { + appendTag("FAMILY", StringUtils.escapeForXML(lastName)); + appendTag("GIVEN", StringUtils.escapeForXML(firstName)); + appendTag("MIDDLE", StringUtils.escapeForXML(middleName)); + } + }); + } + + private void appendTag(String tag, String attr, String attrValue, boolean hasContent, + ContentBuilder builder) { + sb.append('<').append(tag); + if (attr != null) { + sb.append(' ').append(attr).append('=').append('\'').append(attrValue).append('\''); + } + + if (hasContent) { + sb.append('>'); + builder.addTagContent(); + sb.append("</").append(tag).append(">\n"); + } + else { + sb.append("/>\n"); + } + } + + private void appendTag(String tag, boolean hasContent, ContentBuilder builder) { + appendTag(tag, null, null, hasContent, builder); + } + + private void appendTag(String tag, final String tagText) { + if (tagText == null) return; + final ContentBuilder contentBuilder = new ContentBuilder() { + public void addTagContent() { + sb.append(tagText.trim()); + } + }; + appendTag(tag, true, contentBuilder); + } + + } + + //============================================================== + + private interface ContentBuilder { + + void addTagContent(); + } + + //============================================================== +} + diff --git a/src/org/jivesoftware/smackx/packet/Version.java b/src/org/jivesoftware/smackx/packet/Version.java new file mode 100644 index 0000000..41ee419 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/Version.java @@ -0,0 +1,132 @@ +/** + * $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.smackx.packet; + +import org.jivesoftware.smack.packet.IQ; + +/** + * A Version IQ packet, which is used by XMPP clients to discover version information + * about the software running at another entity's JID.<p> + * + * An example to discover the version of the server: + * <pre> + * // Request the version from the server. + * Version versionRequest = new Version(); + * timeRequest.setType(IQ.Type.GET); + * timeRequest.setTo("example.com"); + * + * // Create a packet collector to listen for a response. + * PacketCollector collector = con.createPacketCollector( + * new PacketIDFilter(versionRequest.getPacketID())); + * + * con.sendPacket(versionRequest); + * + * // Wait up to 5 seconds for a result. + * IQ result = (IQ)collector.nextResult(5000); + * if (result != null && result.getType() == IQ.Type.RESULT) { + * Version versionResult = (Version)result; + * // Do something with result... + * }</pre><p> + * + * @author Gaston Dombiak + */ +public class Version extends IQ { + + private String name; + private String version; + private String os; + + /** + * Returns the natural-language name of the software. This property will always be + * present in a result. + * + * @return the natural-language name of the software. + */ + public String getName() { + return name; + } + + /** + * Sets the natural-language name of the software. This message should only be + * invoked when parsing the XML and setting the property to a Version instance. + * + * @param name the natural-language name of the software. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the specific version of the software. This property will always be + * present in a result. + * + * @return the specific version of the software. + */ + public String getVersion() { + return version; + } + + /** + * Sets the specific version of the software. This message should only be + * invoked when parsing the XML and setting the property to a Version instance. + * + * @param version the specific version of the software. + */ + public void setVersion(String version) { + this.version = version; + } + + /** + * Returns the operating system of the queried entity. This property will always be + * present in a result. + * + * @return the operating system of the queried entity. + */ + public String getOs() { + return os; + } + + /** + * Sets the operating system of the queried entity. This message should only be + * invoked when parsing the XML and setting the property to a Version instance. + * + * @param os operating system of the queried entity. + */ + public void setOs(String os) { + this.os = os; + } + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<query xmlns=\"jabber:iq:version\">"); + if (name != null) { + buf.append("<name>").append(name).append("</name>"); + } + if (version != null) { + buf.append("<version>").append(version).append("</version>"); + } + if (os != null) { + buf.append("<os>").append(os).append("</os>"); + } + buf.append("</query>"); + return buf.toString(); + } +} diff --git a/src/org/jivesoftware/smackx/packet/XHTMLExtension.java b/src/org/jivesoftware/smackx/packet/XHTMLExtension.java new file mode 100644 index 0000000..ba5e676 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/XHTMLExtension.java @@ -0,0 +1,126 @@ +/** + * $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.smackx.packet; + +import org.jivesoftware.smack.packet.PacketExtension; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * An XHTML sub-packet, which is used by XMPP clients to exchange formatted text. The XHTML + * extension is only a subset of XHTML 1.0.<p> + * + * The following link summarizes the requirements of XHTML IM: + * <a href="http://www.jabber.org/jeps/jep-0071.html#sect-id2598018">Valid tags</a>.<p> + * + * Warning: this is an non-standard protocol documented by + * <a href="http://www.jabber.org/jeps/jep-0071.html">JEP-71</a>. Because this is a + * non-standard protocol, it is subject to change. + * + * @author Gaston Dombiak + */ +public class XHTMLExtension implements PacketExtension { + + private List<String> bodies = new ArrayList<String>(); + + /** + * Returns the XML element name of the extension sub-packet root element. + * Always returns "html" + * + * @return the XML element name of the packet extension. + */ + public String getElementName() { + return "html"; + } + + /** + * Returns the XML namespace of the extension sub-packet root element. + * According the specification the namespace is always "http://jabber.org/protocol/xhtml-im" + * + * @return the XML namespace of the packet extension. + */ + public String getNamespace() { + return "http://jabber.org/protocol/xhtml-im"; + } + + /** + * Returns the XML representation of a XHTML extension according the specification. + * + * Usually the XML representation will be inside of a Message XML representation like + * in the following example: + * <pre> + * <message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"> + * <subject>Any subject you want</subject> + * <body>This message contains something interesting.</body> + * <html xmlns="http://jabber.org/protocol/xhtml-im"> + * <body><p style='font-size:large'>This message contains something <em>interesting</em>.</p></body> + * </html> + * </message> + * </pre> + * + */ + public String toXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( + "\">"); + // Loop through all the bodies and append them to the string buffer + for (Iterator<String> i = getBodies(); i.hasNext();) { + buf.append(i.next()); + } + buf.append("</").append(getElementName()).append(">"); + return buf.toString(); + } + + /** + * Returns an Iterator for the bodies in the packet. + * + * @return an Iterator for the bodies in the packet. + */ + public Iterator<String> getBodies() { + synchronized (bodies) { + return Collections.unmodifiableList(new ArrayList<String>(bodies)).iterator(); + } + } + + /** + * Adds a body to the packet. + * + * @param body the body to add. + */ + public void addBody(String body) { + synchronized (bodies) { + bodies.add(body); + } + } + + /** + * Returns a count of the bodies in the XHTML packet. + * + * @return the number of bodies in the XHTML packet. + */ + public int getBodiesCount() { + return bodies.size(); + } + +} diff --git a/src/org/jivesoftware/smackx/packet/package.html b/src/org/jivesoftware/smackx/packet/package.html new file mode 100644 index 0000000..490d1d7 --- /dev/null +++ b/src/org/jivesoftware/smackx/packet/package.html @@ -0,0 +1 @@ +<body>XML packets that are part of the XMPP extension protocols.</body>
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/ping/PingFailedListener.java b/src/org/jivesoftware/smackx/ping/PingFailedListener.java new file mode 100644 index 0000000..4cda33b --- /dev/null +++ b/src/org/jivesoftware/smackx/ping/PingFailedListener.java @@ -0,0 +1,21 @@ +/** + * Copyright 2012 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.smackx.ping; + +public interface PingFailedListener { + void pingFailed(); +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/ping/PingManager.java b/src/org/jivesoftware/smackx/ping/PingManager.java new file mode 100644 index 0000000..6b4b48c --- /dev/null +++ b/src/org/jivesoftware/smackx/ping/PingManager.java @@ -0,0 +1,343 @@ +/** + * Copyright 2012-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.smackx.ping; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.ConnectionListener; +import org.jivesoftware.smack.PacketCollector; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPException; +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.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.ping.packet.Ping; +import org.jivesoftware.smackx.ping.packet.Pong; + +/** + * Implements the XMPP Ping as defined by XEP-0199. This protocol offers an + * alternative to the traditional 'white space ping' approach of determining the + * availability of an entity. The XMPP Ping protocol allows ping messages to be + * send in a more XML-friendly approach, which can be used over more than one + * hop in the communication path. + * + * @author Florian Schmaus + * @see <a href="http://www.xmpp.org/extensions/xep-0199.html">XEP-0199:XMPP + * Ping</a> + */ +public class PingManager { + + public static final String NAMESPACE = "urn:xmpp:ping"; + public static final String ELEMENT = "ping"; + + + private static Map<Connection, PingManager> instances = + Collections.synchronizedMap(new WeakHashMap<Connection, PingManager>()); + + static { + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(Connection connection) { + new PingManager(connection); + } + }); + } + + private ScheduledExecutorService periodicPingExecutorService; + private Connection connection; + private int pingInterval = SmackConfiguration.getDefaultPingInterval(); + private Set<PingFailedListener> pingFailedListeners = Collections + .synchronizedSet(new HashSet<PingFailedListener>()); + private ScheduledFuture<?> periodicPingTask; + protected volatile long lastSuccessfulPingByTask = -1; + + + // Ping Flood protection + private long pingMinDelta = 100; + private long lastPingStamp = 0; // timestamp of the last received ping + + // Timestamp of the last pong received, either from the server or another entity + // Note, no need to synchronize this value, it will only increase over time + private long lastSuccessfulManualPing = -1; + + private PingManager(Connection connection) { + ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); + sdm.addFeature(NAMESPACE); + this.connection = connection; + init(); + } + + private void init() { + periodicPingExecutorService = new ScheduledThreadPoolExecutor(1); + PacketFilter pingPacketFilter = new PacketTypeFilter(Ping.class); + connection.addPacketListener(new PacketListener() { + /** + * Sends a Pong for every Ping + */ + public void processPacket(Packet packet) { + if (pingMinDelta > 0) { + // Ping flood protection enabled + long currentMillies = System.currentTimeMillis(); + long delta = currentMillies - lastPingStamp; + lastPingStamp = currentMillies; + if (delta < pingMinDelta) { + return; + } + } + Pong pong = new Pong((Ping)packet); + connection.sendPacket(pong); + } + } + , pingPacketFilter); + connection.addConnectionListener(new ConnectionListener() { + + @Override + public void connectionClosed() { + maybeStopPingServerTask(); + } + + @Override + public void connectionClosedOnError(Exception arg0) { + maybeStopPingServerTask(); + } + + @Override + public void reconnectionSuccessful() { + maybeSchedulePingServerTask(); + } + + @Override + public void reconnectingIn(int seconds) { + } + + @Override + public void reconnectionFailed(Exception e) { + } + }); + instances.put(connection, this); + maybeSchedulePingServerTask(); + } + + public static PingManager getInstanceFor(Connection connection) { + PingManager pingManager = instances.get(connection); + + if (pingManager == null) { + pingManager = new PingManager(connection); + } + + return pingManager; + } + + public void setPingIntervall(int pingIntervall) { + this.pingInterval = pingIntervall; + } + + public int getPingIntervall() { + return pingInterval; + } + + public void registerPingFailedListener(PingFailedListener listener) { + pingFailedListeners.add(listener); + } + + public void unregisterPingFailedListener(PingFailedListener listener) { + pingFailedListeners.remove(listener); + } + + public void disablePingFloodProtection() { + setPingMinimumInterval(-1); + } + + public void setPingMinimumInterval(long ms) { + this.pingMinDelta = ms; + } + + public long getPingMinimumInterval() { + return this.pingMinDelta; + } + + /** + * Pings the given jid and returns the IQ response which is either of + * IQ.Type.ERROR or IQ.Type.RESULT. If we are not connected or if there was + * no reply, null is returned. + * + * You should use isPingSupported(jid) to determine if XMPP Ping is + * supported by the user. + * + * @param jid + * @param pingTimeout + * @return + */ + public IQ ping(String jid, long pingTimeout) { + // Make sure we actually connected to the server + if (!connection.isAuthenticated()) + return null; + + Ping ping = new Ping(connection.getUser(), jid); + + PacketCollector collector = + connection.createPacketCollector(new PacketIDFilter(ping.getPacketID())); + + connection.sendPacket(ping); + + IQ result = (IQ) collector.nextResult(pingTimeout); + + collector.cancel(); + return result; + } + + /** + * Pings the given jid and returns the IQ response with the default + * packet reply timeout + * + * @param jid + * @return + */ + public IQ ping(String jid) { + return ping(jid, SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Pings the given Entity. + * + * Note that XEP-199 shows that if we receive a error response + * service-unavailable there is no way to determine if the response was send + * by the entity (e.g. a user JID) or from a server in between. This is + * intended behavior to avoid presence leaks. + * + * Always use isPingSupported(jid) to determine if XMPP Ping is supported + * by the entity. + * + * @param jid + * @return True if a pong was received, otherwise false + */ + public boolean pingEntity(String jid, long pingTimeout) { + IQ result = ping(jid, pingTimeout); + + if (result == null || result.getType() == IQ.Type.ERROR) { + return false; + } + pongReceived(); + return true; + } + + public boolean pingEntity(String jid) { + return pingEntity(jid, SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Pings the user's server. Will notify the registered + * pingFailedListeners in case of error. + * + * If we receive as response, we can be sure that it came from the server. + * + * @return true if successful, otherwise false + */ + public boolean pingMyServer(long pingTimeout) { + IQ result = ping(connection.getServiceName(), pingTimeout); + + if (result == null) { + for (PingFailedListener l : pingFailedListeners) { + l.pingFailed(); + } + return false; + } + // Maybe not really a pong, but an answer is an answer + pongReceived(); + return true; + } + + /** + * Pings the user's server with the PacketReplyTimeout as defined + * in SmackConfiguration. + * + * @return true if successful, otherwise false + */ + public boolean pingMyServer() { + return pingMyServer(SmackConfiguration.getPacketReplyTimeout()); + } + + /** + * Returns true if XMPP Ping is supported by a given JID + * + * @param jid + * @return + */ + public boolean isPingSupported(String jid) { + try { + DiscoverInfo result = + ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid); + return result.containsFeature(NAMESPACE); + } + catch (XMPPException e) { + return false; + } + } + + /** + * Returns the time of the last successful Ping Pong with the + * users server. If there was no successful Ping (e.g. because this + * feature is disabled) -1 will be returned. + * + * @return + */ + public long getLastSuccessfulPing() { + return Math.max(lastSuccessfulPingByTask, lastSuccessfulManualPing); + } + + protected Set<PingFailedListener> getPingFailedListeners() { + return pingFailedListeners; + } + + /** + * Cancels any existing periodic ping task if there is one and schedules a new ping task if pingInterval is greater + * then zero. + * + */ + protected synchronized void maybeSchedulePingServerTask() { + maybeStopPingServerTask(); + if (pingInterval > 0) { + periodicPingTask = periodicPingExecutorService.schedule(new ServerPingTask(connection), pingInterval, + TimeUnit.SECONDS); + } + } + + private void maybeStopPingServerTask() { + if (periodicPingTask != null) { + periodicPingTask.cancel(true); + periodicPingTask = null; + } + } + + private void pongReceived() { + lastSuccessfulManualPing = System.currentTimeMillis(); + } +} diff --git a/src/org/jivesoftware/smackx/ping/ServerPingTask.java b/src/org/jivesoftware/smackx/ping/ServerPingTask.java new file mode 100644 index 0000000..0901b8f --- /dev/null +++ b/src/org/jivesoftware/smackx/ping/ServerPingTask.java @@ -0,0 +1,77 @@ +/** + * Copyright 2012-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.smackx.ping; + +import java.lang.ref.WeakReference; +import java.util.Set; + +import org.jivesoftware.smack.Connection; + +class ServerPingTask implements Runnable { + + // This has to be a weak reference because IIRC all threads are roots + // for objects and we have a new thread here that should hold a strong + // reference to connection so that it can be GCed. + private WeakReference<Connection> weakConnection; + + private int delta = 1000; // 1 seconds + private int tries = 3; // 3 tries + + protected ServerPingTask(Connection connection) { + this.weakConnection = new WeakReference<Connection>(connection); + } + + public void run() { + Connection connection = weakConnection.get(); + if (connection == null) { + // connection has been collected by GC + // which means we can stop the thread by breaking the loop + return; + } + if (connection.isAuthenticated()) { + PingManager pingManager = PingManager.getInstanceFor(connection); + boolean res = false; + + for (int i = 0; i < tries; i++) { + if (i != 0) { + try { + Thread.sleep(delta); + } catch (InterruptedException e) { + // We received an interrupt + // This only happens if we should stop pinging + return; + } + } + res = pingManager.pingMyServer(); + // stop when we receive a pong back + if (res) { + pingManager.lastSuccessfulPingByTask = System.currentTimeMillis(); + break; + } + } + if (!res) { + Set<PingFailedListener> pingFailedListeners = pingManager.getPingFailedListeners(); + for (PingFailedListener l : pingFailedListeners) { + l.pingFailed(); + } + } else { + // Ping was successful, wind-up the periodic task again + pingManager.maybeSchedulePingServerTask(); + } + } + } +} diff --git a/src/org/jivesoftware/smackx/ping/packet/Ping.java b/src/org/jivesoftware/smackx/ping/packet/Ping.java new file mode 100644 index 0000000..fc5bbdf --- /dev/null +++ b/src/org/jivesoftware/smackx/ping/packet/Ping.java @@ -0,0 +1,38 @@ +/** + * Copyright 2012 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.smackx.ping.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.ping.PingManager; + +public class Ping extends IQ { + + public Ping() { + } + + public Ping(String from, String to) { + setTo(to); + setFrom(from); + setType(IQ.Type.GET); + setPacketID(getPacketID()); + } + + public String getChildElementXML() { + return "<" + PingManager.ELEMENT + " xmlns=\'" + PingManager.NAMESPACE + "\' />"; + } + +} diff --git a/src/org/jivesoftware/smackx/ping/packet/Pong.java b/src/org/jivesoftware/smackx/ping/packet/Pong.java new file mode 100644 index 0000000..9300db0 --- /dev/null +++ b/src/org/jivesoftware/smackx/ping/packet/Pong.java @@ -0,0 +1,45 @@ +/** + * Copyright 2012 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.smackx.ping.packet; + +import org.jivesoftware.smack.packet.IQ; + +public class Pong extends IQ { + + /** + * Composes a Pong packet from a received ping packet. This basically swaps + * the 'from' and 'to' attributes. And sets the IQ type to result. + * + * @param ping + */ + public Pong(Ping ping) { + setType(IQ.Type.RESULT); + setFrom(ping.getTo()); + setTo(ping.getFrom()); + setPacketID(ping.getPacketID()); + } + + /* + * Returns the child element of the Pong reply, which is non-existent. This + * is why we return 'null' here. See e.g. Example 11 from + * http://xmpp.org/extensions/xep-0199.html#e2e + */ + public String getChildElementXML() { + return null; + } + +} diff --git a/src/org/jivesoftware/smackx/ping/provider/PingProvider.java b/src/org/jivesoftware/smackx/ping/provider/PingProvider.java new file mode 100644 index 0000000..ebe7669 --- /dev/null +++ b/src/org/jivesoftware/smackx/ping/provider/PingProvider.java @@ -0,0 +1,32 @@ +/** + * Copyright 2012 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.smackx.ping.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.ping.packet.Ping; +import org.xmlpull.v1.XmlPullParser; + +public class PingProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + // No need to use the ping constructor with arguments. IQ will already + // have filled out all relevant fields ('from', 'to', 'id'). + return new Ping(); + } + +} diff --git a/src/org/jivesoftware/smackx/provider/AdHocCommandDataProvider.java b/src/org/jivesoftware/smackx/provider/AdHocCommandDataProvider.java new file mode 100755 index 0000000..63d24ec --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/AdHocCommandDataProvider.java @@ -0,0 +1,155 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2005-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.smackx.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.commands.AdHocCommand;
+import org.jivesoftware.smackx.commands.AdHocCommand.Action;
+import org.jivesoftware.smackx.commands.AdHocCommandNote;
+import org.jivesoftware.smackx.packet.AdHocCommandData;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The AdHocCommandDataProvider parses AdHocCommandData packets.
+ *
+ * @author Gabriel Guardincerri
+ */
+public class AdHocCommandDataProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ AdHocCommandData adHocCommandData = new AdHocCommandData();
+ DataFormProvider dataFormProvider = new DataFormProvider();
+
+ int eventType;
+ String elementName;
+ String namespace;
+ adHocCommandData.setSessionID(parser.getAttributeValue("", "sessionid"));
+ adHocCommandData.setNode(parser.getAttributeValue("", "node"));
+
+ // Status
+ String status = parser.getAttributeValue("", "status");
+ if (AdHocCommand.Status.executing.toString().equalsIgnoreCase(status)) {
+ adHocCommandData.setStatus(AdHocCommand.Status.executing);
+ }
+ else if (AdHocCommand.Status.completed.toString().equalsIgnoreCase(status)) {
+ adHocCommandData.setStatus(AdHocCommand.Status.completed);
+ }
+ else if (AdHocCommand.Status.canceled.toString().equalsIgnoreCase(status)) {
+ adHocCommandData.setStatus(AdHocCommand.Status.canceled);
+ }
+
+ // Action
+ String action = parser.getAttributeValue("", "action");
+ if (action != null) {
+ Action realAction = AdHocCommand.Action.valueOf(action);
+ if (realAction == null || realAction.equals(Action.unknown)) {
+ adHocCommandData.setAction(Action.unknown);
+ }
+ else {
+ adHocCommandData.setAction(realAction);
+ }
+ }
+ while (!done) {
+ eventType = parser.next();
+ elementName = parser.getName();
+ namespace = parser.getNamespace();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("actions")) {
+ String execute = parser.getAttributeValue("", "execute");
+ if (execute != null) {
+ adHocCommandData.setExecuteAction(AdHocCommand.Action.valueOf(execute));
+ }
+ }
+ else if (parser.getName().equals("next")) {
+ adHocCommandData.addAction(AdHocCommand.Action.next);
+ }
+ else if (parser.getName().equals("complete")) {
+ adHocCommandData.addAction(AdHocCommand.Action.complete);
+ }
+ else if (parser.getName().equals("prev")) {
+ adHocCommandData.addAction(AdHocCommand.Action.prev);
+ }
+ else if (elementName.equals("x") && namespace.equals("jabber:x:data")) {
+ adHocCommandData.setForm((DataForm) dataFormProvider.parseExtension(parser));
+ }
+ else if (parser.getName().equals("note")) {
+ AdHocCommandNote.Type type = AdHocCommandNote.Type.valueOf(
+ parser.getAttributeValue("", "type"));
+ String value = parser.nextText();
+ adHocCommandData.addNote(new AdHocCommandNote(type, value));
+ }
+ else if (parser.getName().equals("error")) {
+ XMPPError error = PacketParserUtils.parseError(parser);
+ adHocCommandData.setError(error);
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("command")) {
+ done = true;
+ }
+ }
+ }
+ return adHocCommandData;
+ }
+
+ public static class BadActionError implements PacketExtensionProvider {
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badAction);
+ }
+ }
+
+ public static class MalformedActionError implements PacketExtensionProvider {
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.malformedAction);
+ }
+ }
+
+ public static class BadLocaleError implements PacketExtensionProvider {
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badLocale);
+ }
+ }
+
+ public static class BadPayloadError implements PacketExtensionProvider {
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badPayload);
+ }
+ }
+
+ public static class BadSessionIDError implements PacketExtensionProvider {
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badSessionid);
+ }
+ }
+
+ public static class SessionExpiredError implements PacketExtensionProvider {
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.sessionExpired);
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/provider/CapsExtensionProvider.java b/src/org/jivesoftware/smackx/provider/CapsExtensionProvider.java new file mode 100644 index 0000000..5a7cd2f --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/CapsExtensionProvider.java @@ -0,0 +1,65 @@ +/* + * Copyright 2009 Jonas Ã…dahl. + * Copyright 2011-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.smackx.provider; + +import java.io.IOException; + +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.entitycaps.packet.CapsExtension; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +public class CapsExtensionProvider implements PacketExtensionProvider { + private static final int MAX_DEPTH = 10; + + public PacketExtension parseExtension(XmlPullParser parser) throws XmlPullParserException, IOException, + XMPPException { + String hash = null; + String version = null; + String node = null; + int depth = 0; + while (true) { + if (parser.getEventType() == XmlPullParser.START_TAG && parser.getName().equalsIgnoreCase("c")) { + hash = parser.getAttributeValue(null, "hash"); + version = parser.getAttributeValue(null, "ver"); + node = parser.getAttributeValue(null, "node"); + } + + if (parser.getEventType() == XmlPullParser.END_TAG && parser.getName().equalsIgnoreCase("c")) { + break; + } else { + parser.next(); + } + + if (depth < MAX_DEPTH) { + depth++; + } else { + throw new XMPPException("Malformed caps element"); + } + } + + if (hash != null && version != null && node != null) { + return new CapsExtension(node, version, hash); + } else { + throw new XMPPException("Caps elment with missing attributes"); + } + } +} diff --git a/src/org/jivesoftware/smackx/provider/DataFormProvider.java b/src/org/jivesoftware/smackx/provider/DataFormProvider.java new file mode 100644 index 0000000..c13f234 --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/DataFormProvider.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.smackx.provider; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.packet.DataForm; +import org.xmlpull.v1.XmlPullParser; + +import java.util.ArrayList; +import java.util.List; + +/** + * The DataFormProvider parses DataForm packets. + * + * @author Gaston Dombiak + */ +public class DataFormProvider implements PacketExtensionProvider { + + /** + * Creates a new DataFormProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor + */ + public DataFormProvider() { + } + + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + boolean done = false; + StringBuilder buffer = null; + DataForm dataForm = new DataForm(parser.getAttributeValue("", "type")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("instructions")) { + dataForm.addInstruction(parser.nextText()); + } + else if (parser.getName().equals("title")) { + dataForm.setTitle(parser.nextText()); + } + else if (parser.getName().equals("field")) { + dataForm.addField(parseField(parser)); + } + else if (parser.getName().equals("item")) { + dataForm.addItem(parseItem(parser)); + } + else if (parser.getName().equals("reported")) { + dataForm.setReportedData(parseReported(parser)); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(dataForm.getElementName())) { + done = true; + } + } + } + return dataForm; + } + + private FormField parseField(XmlPullParser parser) throws Exception { + boolean done = false; + FormField formField = new FormField(parser.getAttributeValue("", "var")); + formField.setLabel(parser.getAttributeValue("", "label")); + formField.setType(parser.getAttributeValue("", "type")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("desc")) { + formField.setDescription(parser.nextText()); + } + else if (parser.getName().equals("value")) { + formField.addValue(parser.nextText()); + } + else if (parser.getName().equals("required")) { + formField.setRequired(true); + } + else if (parser.getName().equals("option")) { + formField.addOption(parseOption(parser)); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("field")) { + done = true; + } + } + } + return formField; + } + + private DataForm.Item parseItem(XmlPullParser parser) throws Exception { + boolean done = false; + List<FormField> fields = new ArrayList<FormField>(); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("field")) { + fields.add(parseField(parser)); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + done = true; + } + } + } + return new DataForm.Item(fields); + } + + private DataForm.ReportedData parseReported(XmlPullParser parser) throws Exception { + boolean done = false; + List<FormField> fields = new ArrayList<FormField>(); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("field")) { + fields.add(parseField(parser)); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("reported")) { + done = true; + } + } + } + return new DataForm.ReportedData(fields); + } + + private FormField.Option parseOption(XmlPullParser parser) throws Exception { + boolean done = false; + FormField.Option option = null; + String label = parser.getAttributeValue("", "label"); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("value")) { + option = new FormField.Option(label, parser.nextText()); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("option")) { + done = true; + } + } + } + return option; + } +} diff --git a/src/org/jivesoftware/smackx/provider/DelayInfoProvider.java b/src/org/jivesoftware/smackx/provider/DelayInfoProvider.java new file mode 100644 index 0000000..6fa52b7 --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/DelayInfoProvider.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.smackx.provider; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smackx.packet.DelayInfo; +import org.jivesoftware.smackx.packet.DelayInformation; +import org.xmlpull.v1.XmlPullParser; + +/** + * This provider simply creates a {@link DelayInfo} decorator for the {@link DelayInformation} that + * is returned by the superclass. This allows the new code using + * <a href="http://xmpp.org/extensions/xep-0203.html">Delay Information XEP-0203</a> to be + * backward compatible with <a href="http://xmpp.org/extensions/xep-0091.html">XEP-0091</a>. + * + * <p>This provider must be registered in the <b>smack.properties</b> file for the element + * <b>delay</b> with namespace <b>urn:xmpp:delay</b></p> + * + * @author Robin Collier + */ +public class DelayInfoProvider extends DelayInformationProvider +{ + + @Override + public PacketExtension parseExtension(XmlPullParser parser) throws Exception + { + return new DelayInfo((DelayInformation)super.parseExtension(parser)); + } + +} diff --git a/src/org/jivesoftware/smackx/provider/DelayInformationProvider.java b/src/org/jivesoftware/smackx/provider/DelayInformationProvider.java new file mode 100644 index 0000000..e5fe010 --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/DelayInformationProvider.java @@ -0,0 +1,74 @@ +/** + * $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.smackx.provider; + +import java.text.ParseException; +import java.util.Date; + +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.packet.DelayInformation; +import org.xmlpull.v1.XmlPullParser; + +/** + * The DelayInformationProvider parses DelayInformation packets. + * + * @author Gaston Dombiak + * @author Henning Staib + */ +public class DelayInformationProvider implements PacketExtensionProvider { + + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + String stampString = (parser.getAttributeValue("", "stamp")); + Date stamp = null; + + try { + stamp = StringUtils.parseDate(stampString); + } + catch (ParseException parseExc) { + /* + * if date could not be parsed but XML is valid, don't shutdown + * connection by throwing an exception instead set timestamp to epoch + * so that it is obviously wrong. + */ + if (stamp == null) { + stamp = new Date(0); + } + } + + + DelayInformation delayInformation = new DelayInformation(stamp); + delayInformation.setFrom(parser.getAttributeValue("", "from")); + String reason = parser.nextText(); + + /* + * parser.nextText() returns empty string if there is no reason. + * DelayInformation API specifies that null should be returned in that + * case. + */ + reason = "".equals(reason) ? null : reason; + delayInformation.setReason(reason); + + return delayInformation; + } +} diff --git a/src/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java b/src/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java new file mode 100644 index 0000000..6ad6fef --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java @@ -0,0 +1,86 @@ +/** + * $RCSfile$ + * $Revision: 7071 $ + * $Date: 2007-02-12 08:59:05 +0800 (Mon, 12 Feb 2007) $ + * + * 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.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.xmlpull.v1.XmlPullParser; + +/** +* The DiscoverInfoProvider parses Service Discovery information packets. +* +* @author Gaston Dombiak +*/ +public class DiscoverInfoProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + DiscoverInfo discoverInfo = new DiscoverInfo(); + boolean done = false; + DiscoverInfo.Feature feature = null; + DiscoverInfo.Identity identity = null; + String category = ""; + String name = ""; + String type = ""; + String variable = ""; + String lang = ""; + discoverInfo.setNode(parser.getAttributeValue("", "node")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("identity")) { + // Initialize the variables from the parsed XML + category = parser.getAttributeValue("", "category"); + name = parser.getAttributeValue("", "name"); + type = parser.getAttributeValue("", "type"); + lang = parser.getAttributeValue(parser.getNamespace("xml"), "lang"); + } + else if (parser.getName().equals("feature")) { + // Initialize the variables from the parsed XML + variable = parser.getAttributeValue("", "var"); + } + // Otherwise, it must be a packet extension. + else { + discoverInfo.addExtension(PacketParserUtils.parsePacketExtension(parser + .getName(), parser.getNamespace(), parser)); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("identity")) { + // Create a new identity and add it to the discovered info. + identity = new DiscoverInfo.Identity(category, name, type); + if (lang != null) + identity.setLanguage(lang); + discoverInfo.addIdentity(identity); + } + if (parser.getName().equals("feature")) { + // Create a new feature and add it to the discovered info. + discoverInfo.addFeature(variable); + } + if (parser.getName().equals("query")) { + done = true; + } + } + } + + return discoverInfo; + } +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/provider/DiscoverItemsProvider.java b/src/org/jivesoftware/smackx/provider/DiscoverItemsProvider.java new file mode 100644 index 0000000..fcbe25f --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/DiscoverItemsProvider.java @@ -0,0 +1,69 @@ +/** + * $RCSfile$ + * $Revision: 7071 $ + * $Date: 2007-02-12 08:59:05 +0800 (Mon, 12 Feb 2007) $ + * + * 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.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.packet.*; +import org.xmlpull.v1.XmlPullParser; + +/** +* The DiscoverInfoProvider parses Service Discovery items packets. +* +* @author Gaston Dombiak +*/ +public class DiscoverItemsProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + DiscoverItems discoverItems = new DiscoverItems(); + boolean done = false; + DiscoverItems.Item item; + String jid = ""; + String name = ""; + String action = ""; + String node = ""; + discoverItems.setNode(parser.getAttributeValue("", "node")); + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG && "item".equals(parser.getName())) { + // Initialize the variables from the parsed XML + jid = parser.getAttributeValue("", "jid"); + name = parser.getAttributeValue("", "name"); + node = parser.getAttributeValue("", "node"); + action = parser.getAttributeValue("", "action"); + } + else if (eventType == XmlPullParser.END_TAG && "item".equals(parser.getName())) { + // Create a new Item and add it to DiscoverItems. + item = new DiscoverItems.Item(jid); + item.setName(name); + item.setNode(node); + item.setAction(action); + discoverItems.addItem(item); + } + else if (eventType == XmlPullParser.END_TAG && "query".equals(parser.getName())) { + done = true; + } + } + + return discoverItems; + } +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/provider/EmbeddedExtensionProvider.java b/src/org/jivesoftware/smackx/provider/EmbeddedExtensionProvider.java new file mode 100644 index 0000000..3d5ceb4 --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/EmbeddedExtensionProvider.java @@ -0,0 +1,111 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jivesoftware.smackx.provider;
+
+import java.util.ArrayList;
+import java.util.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
+ *
+ * @deprecated This has been moved to {@link org.jivesoftware.smack.provider.EmbeddedExtensionProvider}
+ */
+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/smackx/provider/HeaderProvider.java b/src/org/jivesoftware/smackx/provider/HeaderProvider.java new file mode 100644 index 0000000..7344880 --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/HeaderProvider.java @@ -0,0 +1,44 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.packet.Header;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Parses the header element as defined in <a href="http://xmpp.org/extensions/xep-0131">Stanza Headers and Internet Metadata (SHIM)</a>.
+ *
+ * @author Robin Collier
+ */
+public class HeaderProvider implements PacketExtensionProvider
+{
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception
+ {
+ String name = parser.getAttributeValue(null, "name");
+ String value = null;
+
+ parser.next();
+
+ if (parser.getEventType() == XmlPullParser.TEXT)
+ value = parser.getText();
+
+ while(parser.getEventType() != XmlPullParser.END_TAG)
+ parser.next();
+
+ return new Header(name, value);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/provider/HeadersProvider.java b/src/org/jivesoftware/smackx/provider/HeadersProvider.java new file mode 100644 index 0000000..056dd58 --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/HeadersProvider.java @@ -0,0 +1,37 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.provider;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.packet.Header;
+import org.jivesoftware.smackx.packet.HeadersExtension;
+
+/**
+ * Parses the headers element as defined in <a href="http://xmpp.org/extensions/xep-0131">Stanza Headers and Internet Metadata (SHIM)</a>.
+ *
+ * @author Robin Collier
+ */
+public class HeadersProvider extends EmbeddedExtensionProvider
+{
+ @Override
+ protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
+ {
+ return new HeadersExtension((Collection<Header>)content);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/provider/MUCAdminProvider.java b/src/org/jivesoftware/smackx/provider/MUCAdminProvider.java new file mode 100644 index 0000000..1072232 --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/MUCAdminProvider.java @@ -0,0 +1,81 @@ +/** + * $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.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.packet.MUCAdmin; +import org.xmlpull.v1.XmlPullParser; + +/** + * The MUCAdminProvider parses MUCAdmin packets. (@see MUCAdmin) + * + * @author Gaston Dombiak + */ +public class MUCAdminProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + MUCAdmin mucAdmin = new MUCAdmin(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + mucAdmin.addItem(parseItem(parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + + return mucAdmin; + } + + private MUCAdmin.Item parseItem(XmlPullParser parser) throws Exception { + boolean done = false; + MUCAdmin.Item item = + new MUCAdmin.Item( + parser.getAttributeValue("", "affiliation"), + parser.getAttributeValue("", "role")); + item.setNick(parser.getAttributeValue("", "nick")); + item.setJid(parser.getAttributeValue("", "jid")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("actor")) { + item.setActor(parser.getAttributeValue("", "jid")); + } + if (parser.getName().equals("reason")) { + item.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + done = true; + } + } + } + return item; + } +} diff --git a/src/org/jivesoftware/smackx/provider/MUCOwnerProvider.java b/src/org/jivesoftware/smackx/provider/MUCOwnerProvider.java new file mode 100644 index 0000000..ff3094e --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/MUCOwnerProvider.java @@ -0,0 +1,108 @@ +/** + * $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.smackx.provider; + +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smack.provider.*; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.packet.MUCOwner; +import org.xmlpull.v1.XmlPullParser; + +/** + * The MUCOwnerProvider parses MUCOwner packets. (@see MUCOwner) + * + * @author Gaston Dombiak + */ +public class MUCOwnerProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + MUCOwner mucOwner = new MUCOwner(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + mucOwner.addItem(parseItem(parser)); + } + else if (parser.getName().equals("destroy")) { + mucOwner.setDestroy(parseDestroy(parser)); + } + // Otherwise, it must be a packet extension. + else { + mucOwner.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(), + parser.getNamespace(), parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + + return mucOwner; + } + + private MUCOwner.Item parseItem(XmlPullParser parser) throws Exception { + boolean done = false; + MUCOwner.Item item = new MUCOwner.Item(parser.getAttributeValue("", "affiliation")); + item.setNick(parser.getAttributeValue("", "nick")); + item.setRole(parser.getAttributeValue("", "role")); + item.setJid(parser.getAttributeValue("", "jid")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("actor")) { + item.setActor(parser.getAttributeValue("", "jid")); + } + if (parser.getName().equals("reason")) { + item.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + done = true; + } + } + } + return item; + } + + private MUCOwner.Destroy parseDestroy(XmlPullParser parser) throws Exception { + boolean done = false; + MUCOwner.Destroy destroy = new MUCOwner.Destroy(); + destroy.setJid(parser.getAttributeValue("", "jid")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("reason")) { + destroy.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("destroy")) { + done = true; + } + } + } + return destroy; + } +} diff --git a/src/org/jivesoftware/smackx/provider/MUCUserProvider.java b/src/org/jivesoftware/smackx/provider/MUCUserProvider.java new file mode 100644 index 0000000..5a98af6 --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/MUCUserProvider.java @@ -0,0 +1,174 @@ +/** + * $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.smackx.provider; + +import org.jivesoftware.smack.packet.*; +import org.jivesoftware.smack.provider.*; +import org.jivesoftware.smackx.packet.*; +import org.xmlpull.v1.XmlPullParser; + +/** + * The MUCUserProvider parses packets with extended presence information about + * roles and affiliations. + * + * @author Gaston Dombiak + */ +public class MUCUserProvider implements PacketExtensionProvider { + + /** + * Creates a new MUCUserProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument + * constructor + */ + public MUCUserProvider() { + } + + /** + * Parses a MUCUser packet (extension sub-packet). + * + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + MUCUser mucUser = new MUCUser(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("invite")) { + mucUser.setInvite(parseInvite(parser)); + } + if (parser.getName().equals("item")) { + mucUser.setItem(parseItem(parser)); + } + if (parser.getName().equals("password")) { + mucUser.setPassword(parser.nextText()); + } + if (parser.getName().equals("status")) { + mucUser.setStatus(new MUCUser.Status(parser.getAttributeValue("", "code"))); + } + if (parser.getName().equals("decline")) { + mucUser.setDecline(parseDecline(parser)); + } + if (parser.getName().equals("destroy")) { + mucUser.setDestroy(parseDestroy(parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("x")) { + done = true; + } + } + } + + return mucUser; + } + + private MUCUser.Item parseItem(XmlPullParser parser) throws Exception { + boolean done = false; + MUCUser.Item item = + new MUCUser.Item( + parser.getAttributeValue("", "affiliation"), + parser.getAttributeValue("", "role")); + item.setNick(parser.getAttributeValue("", "nick")); + item.setJid(parser.getAttributeValue("", "jid")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("actor")) { + item.setActor(parser.getAttributeValue("", "jid")); + } + if (parser.getName().equals("reason")) { + item.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + done = true; + } + } + } + return item; + } + + private MUCUser.Invite parseInvite(XmlPullParser parser) throws Exception { + boolean done = false; + MUCUser.Invite invite = new MUCUser.Invite(); + invite.setFrom(parser.getAttributeValue("", "from")); + invite.setTo(parser.getAttributeValue("", "to")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("reason")) { + invite.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("invite")) { + done = true; + } + } + } + return invite; + } + + private MUCUser.Decline parseDecline(XmlPullParser parser) throws Exception { + boolean done = false; + MUCUser.Decline decline = new MUCUser.Decline(); + decline.setFrom(parser.getAttributeValue("", "from")); + decline.setTo(parser.getAttributeValue("", "to")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("reason")) { + decline.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("decline")) { + done = true; + } + } + } + return decline; + } + + private MUCUser.Destroy parseDestroy(XmlPullParser parser) throws Exception { + boolean done = false; + MUCUser.Destroy destroy = new MUCUser.Destroy(); + destroy.setJid(parser.getAttributeValue("", "jid")); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("reason")) { + destroy.setReason(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("destroy")) { + done = true; + } + } + } + return destroy; + } +} diff --git a/src/org/jivesoftware/smackx/provider/MessageEventProvider.java b/src/org/jivesoftware/smackx/provider/MessageEventProvider.java new file mode 100644 index 0000000..b631546 --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/MessageEventProvider.java @@ -0,0 +1,77 @@ +/** + * $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.smackx.provider; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.packet.MessageEvent; +import org.xmlpull.v1.XmlPullParser; + +/** + * + * The MessageEventProvider parses Message Event packets. +* + * @author Gaston Dombiak + */ +public class MessageEventProvider implements PacketExtensionProvider { + + /** + * Creates a new MessageEventProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor + */ + public MessageEventProvider() { + } + + /** + * Parses a MessageEvent packet (extension sub-packet). + * + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public PacketExtension parseExtension(XmlPullParser parser) + throws Exception { + MessageEvent messageEvent = new MessageEvent(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("id")) + messageEvent.setPacketID(parser.nextText()); + if (parser.getName().equals(MessageEvent.COMPOSING)) + messageEvent.setComposing(true); + if (parser.getName().equals(MessageEvent.DELIVERED)) + messageEvent.setDelivered(true); + if (parser.getName().equals(MessageEvent.DISPLAYED)) + messageEvent.setDisplayed(true); + if (parser.getName().equals(MessageEvent.OFFLINE)) + messageEvent.setOffline(true); + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("x")) { + done = true; + } + } + } + + return messageEvent; + } + +} diff --git a/src/org/jivesoftware/smackx/provider/MultipleAddressesProvider.java b/src/org/jivesoftware/smackx/provider/MultipleAddressesProvider.java new file mode 100644 index 0000000..4c3e356 --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/MultipleAddressesProvider.java @@ -0,0 +1,67 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2006 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.provider; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.packet.MultipleAddresses; +import org.xmlpull.v1.XmlPullParser; + +/** + * The MultipleAddressesProvider parses {@link MultipleAddresses} packets. + * + * @author Gaston Dombiak + */ +public class MultipleAddressesProvider implements PacketExtensionProvider { + + /** + * Creates a new MultipleAddressesProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument + * constructor. + */ + public MultipleAddressesProvider() { + } + + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + boolean done = false; + MultipleAddresses multipleAddresses = new MultipleAddresses(); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("address")) { + String type = parser.getAttributeValue("", "type"); + String jid = parser.getAttributeValue("", "jid"); + String node = parser.getAttributeValue("", "node"); + String desc = parser.getAttributeValue("", "desc"); + boolean delivered = "true".equals(parser.getAttributeValue("", "delivered")); + String uri = parser.getAttributeValue("", "uri"); + // Add the parsed address + multipleAddresses.addAddress(type, jid, node, desc, delivered, uri); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(multipleAddresses.getElementName())) { + done = true; + } + } + } + return multipleAddresses; + } +} diff --git a/src/org/jivesoftware/smackx/provider/PEPProvider.java b/src/org/jivesoftware/smackx/provider/PEPProvider.java new file mode 100644 index 0000000..f33dcde --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/PEPProvider.java @@ -0,0 +1,93 @@ +/** + * $RCSfile: PEPProvider.java,v $ + * $Revision: 1.2 $ + * $Date: 2007/11/06 02:05:09 $ + * + * 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.smackx.provider; + +import java.util.HashMap; +import java.util.Map; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.xmlpull.v1.XmlPullParser; + +/** + * + * The PEPProvider parses incoming PEPEvent packets. + * (XEP-163 has a weird asymmetric deal: outbound PEP are <iq> + <pubsub> and inbound are <message> + <event>. + * The provider only deals with inbound, and so it only deals with <message>. + * + * Anyhoo... + * + * The way this works is that PEPxxx classes are generic <pubsub> and <message> providers, and anyone who + * wants to publish/receive PEPs, such as <tune>, <geoloc>, etc., simply need to extend PEPItem and register (here) + * a PacketExtensionProvider that knows how to parse that PEPItem extension. + * + * @author Jeff Williams + */ +public class PEPProvider implements PacketExtensionProvider { + + Map<String, PacketExtensionProvider> nodeParsers = new HashMap<String, PacketExtensionProvider>(); + PacketExtension pepItem; + + /** + * Creates a new PEPProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor + */ + public PEPProvider() { + } + + public void registerPEPParserExtension(String node, PacketExtensionProvider pepItemParser) { + nodeParsers.put(node, pepItemParser); + } + + /** + * Parses a PEPEvent packet and extracts a PEPItem from it. + * (There is only one per <event>.) + * + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("event")) { + } else if (parser.getName().equals("items")) { + // Figure out the node for this event. + String node = parser.getAttributeValue("", "node"); + // Get the parser for this kind of node, and if found then parse the node. + PacketExtensionProvider nodeParser = nodeParsers.get(node); + if (nodeParser != null) { + pepItem = nodeParser.parseExtension(parser); + } + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("event")) { + done = true; + } + } + } + + return pepItem; + } +} diff --git a/src/org/jivesoftware/smackx/provider/PrivateDataProvider.java b/src/org/jivesoftware/smackx/provider/PrivateDataProvider.java new file mode 100644 index 0000000..b781a5a --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/PrivateDataProvider.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.smackx.provider; + +import org.xmlpull.v1.XmlPullParser; +import org.jivesoftware.smackx.packet.PrivateData; + +/** + * An interface for parsing custom private data. Each PrivateDataProvider must + * be registered with the PrivateDataManager class for it to be used. Every implementation + * of this interface <b>must</b> have a public, no-argument constructor. + * + * @author Matt Tucker + */ +public interface PrivateDataProvider { + + /** + * Parse the private data sub-document and create a PrivateData instance. At the + * beginning of the method call, the xml parser will be positioned at the opening + * tag of the private data child element. At the end of the method call, the parser + * <b>must</b> be positioned on the closing tag of the child element. + * + * @param parser an XML parser. + * @return a new PrivateData instance. + * @throws Exception if an error occurs parsing the XML. + */ + public PrivateData parsePrivateData(XmlPullParser parser) throws Exception; +} diff --git a/src/org/jivesoftware/smackx/provider/RosterExchangeProvider.java b/src/org/jivesoftware/smackx/provider/RosterExchangeProvider.java new file mode 100644 index 0000000..76e09be --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/RosterExchangeProvider.java @@ -0,0 +1,90 @@ +/** + * $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.smackx.provider; + +import java.util.ArrayList; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smackx.*; +import org.jivesoftware.smackx.packet.*; +import org.xmlpull.v1.XmlPullParser; + +/** + * + * The RosterExchangeProvider parses RosterExchange packets. + * + * @author Gaston Dombiak + */ +public class RosterExchangeProvider implements PacketExtensionProvider { + + /** + * Creates a new RosterExchangeProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor + */ + public RosterExchangeProvider() { + } + + /** + * Parses a RosterExchange packet (extension sub-packet). + * + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public PacketExtension parseExtension(XmlPullParser parser) throws Exception { + + RosterExchange rosterExchange = new RosterExchange(); + boolean done = false; + RemoteRosterEntry remoteRosterEntry = null; + String jid = ""; + String name = ""; + ArrayList<String> groupsName = new ArrayList<String>(); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + // Reset this variable since they are optional for each item + groupsName = new ArrayList<String>(); + // Initialize the variables from the parsed XML + jid = parser.getAttributeValue("", "jid"); + name = parser.getAttributeValue("", "name"); + } + if (parser.getName().equals("group")) { + groupsName.add(parser.nextText()); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + // Create packet. + remoteRosterEntry = new RemoteRosterEntry(jid, name, (String[]) groupsName.toArray(new String[groupsName.size()])); + rosterExchange.addRosterEntry(remoteRosterEntry); + } + if (parser.getName().equals("x")) { + done = true; + } + } + } + + return rosterExchange; + + } + +} diff --git a/src/org/jivesoftware/smackx/provider/StreamInitiationProvider.java b/src/org/jivesoftware/smackx/provider/StreamInitiationProvider.java new file mode 100644 index 0000000..8101e4c --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/StreamInitiationProvider.java @@ -0,0 +1,124 @@ +/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.provider;
+
+import java.text.ParseException;
+import java.util.Date;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.packet.DataForm; +import org.jivesoftware.smackx.packet.StreamInitiation; +import org.jivesoftware.smackx.packet.StreamInitiation.File; +import org.xmlpull.v1.XmlPullParser; + +/** + * The StreamInitiationProvider parses StreamInitiation packets. + * + * @author Alexander Wenckus + * + */ +public class StreamInitiationProvider implements IQProvider { + + public IQ parseIQ(final XmlPullParser parser) throws Exception { + boolean done = false; + + // si + String id = parser.getAttributeValue("", "id"); + String mimeType = parser.getAttributeValue("", "mime-type"); + + StreamInitiation initiation = new StreamInitiation(); + + // file + String name = null; + String size = null; + String hash = null; + String date = null; + String desc = null; + boolean isRanged = false; + + // feature + DataForm form = null; + DataFormProvider dataFormProvider = new DataFormProvider(); + + int eventType; + String elementName; + String namespace; + while (!done) { + eventType = parser.next(); + elementName = parser.getName(); + namespace = parser.getNamespace(); + if (eventType == XmlPullParser.START_TAG) { + if (elementName.equals("file")) { + name = parser.getAttributeValue("", "name"); + size = parser.getAttributeValue("", "size"); + hash = parser.getAttributeValue("", "hash"); + date = parser.getAttributeValue("", "date"); + } else if (elementName.equals("desc")) { + desc = parser.nextText(); + } else if (elementName.equals("range")) { + isRanged = true; + } else if (elementName.equals("x") + && namespace.equals("jabber:x:data")) { + form = (DataForm) dataFormProvider.parseExtension(parser); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (elementName.equals("si")) { + done = true; + } else if (elementName.equals("file")) { + long fileSize = 0; + if(size != null && size.trim().length() !=0){ + try { + fileSize = Long.parseLong(size); + } + catch (NumberFormatException e) { + e.printStackTrace(); + } + } + + Date fileDate = new Date(); + if (date != null) { + try { + fileDate = StringUtils.parseXEP0082Date(date);
+ } catch (ParseException e) { + // couldn't parse date, use current date-time + } + } + + File file = new File(name, fileSize); + file.setHash(hash); + file.setDate(fileDate); + file.setDesc(desc); + file.setRanged(isRanged); + initiation.setFile(file); + } + } + } + + initiation.setSesssionID(id); + initiation.setMimeType(mimeType); + + initiation.setFeatureNegotiationForm(form); + + return initiation; + } + +} diff --git a/src/org/jivesoftware/smackx/provider/VCardProvider.java b/src/org/jivesoftware/smackx/provider/VCardProvider.java new file mode 100644 index 0000000..8fa0421 --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/VCardProvider.java @@ -0,0 +1,293 @@ +/** + * $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.smackx.provider; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.packet.VCard; +import org.w3c.dom.*; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * vCard provider. + * + * @author Gaston Dombiak + * @author Derek DeMoro + */ +public class VCardProvider implements IQProvider { + + private static final String PREFERRED_ENCODING = "UTF-8"; + + public IQ parseIQ(XmlPullParser parser) throws Exception { + final StringBuilder sb = new StringBuilder(); + try { + int event = parser.getEventType(); + // get the content + while (true) { + switch (event) { + case XmlPullParser.TEXT: + // We must re-escape the xml so that the DOM won't throw an exception + sb.append(StringUtils.escapeForXML(parser.getText())); + break; + case XmlPullParser.START_TAG: + sb.append('<').append(parser.getName()).append('>'); + break; + case XmlPullParser.END_TAG: + sb.append("</").append(parser.getName()).append('>'); + break; + default: + } + + if (event == XmlPullParser.END_TAG && "vCard".equals(parser.getName())) break; + + event = parser.next(); + } + } + catch (XmlPullParserException e) { + e.printStackTrace(); + } + catch (IOException e) { + e.printStackTrace(); + } + + String xmlText = sb.toString(); + return createVCardFromXML(xmlText); + } + + /** + * Builds a users vCard from xml file. + * + * @param xml the xml representing a users vCard. + * @return the VCard. + * @throws Exception if an exception occurs. + */ + public static VCard createVCardFromXML(String xml) throws Exception { + VCard vCard = new VCard(); + + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document document = documentBuilder.parse( + new ByteArrayInputStream(xml.getBytes(PREFERRED_ENCODING))); + + new VCardReader(vCard, document).initializeFields(); + return vCard; + } + + private static class VCardReader { + + private final VCard vCard; + private final Document document; + + VCardReader(VCard vCard, Document document) { + this.vCard = vCard; + this.document = document; + } + + public void initializeFields() { + vCard.setFirstName(getTagContents("GIVEN")); + vCard.setLastName(getTagContents("FAMILY")); + vCard.setMiddleName(getTagContents("MIDDLE")); + setupPhoto(); + + setupEmails(); + + vCard.setOrganization(getTagContents("ORGNAME")); + vCard.setOrganizationUnit(getTagContents("ORGUNIT")); + + setupSimpleFields(); + + setupPhones(); + setupAddresses(); + } + + private void setupPhoto() { + String binval = null; + String mimetype = null; + + NodeList photo = document.getElementsByTagName("PHOTO"); + if (photo.getLength() != 1) + return; + + Node photoNode = photo.item(0); + NodeList childNodes = photoNode.getChildNodes(); + + int childNodeCount = childNodes.getLength(); + List<Node> nodes = new ArrayList<Node>(childNodeCount); + for (int i = 0; i < childNodeCount; i++) + nodes.add(childNodes.item(i)); + + String name = null; + String value = null; + for (Node n : nodes) { + name = n.getNodeName(); + value = n.getTextContent(); + if (name.equals("BINVAL")) { + binval = value; + } + else if (name.equals("TYPE")) { + mimetype = value; + } + } + + if (binval == null || mimetype == null) + return; + + vCard.setAvatar(binval, mimetype); + } + + private void setupEmails() { + NodeList nodes = document.getElementsByTagName("USERID"); + if (nodes == null) return; + for (int i = 0; i < nodes.getLength(); i++) { + Element element = (Element) nodes.item(i); + if ("WORK".equals(element.getParentNode().getFirstChild().getNodeName())) { + vCard.setEmailWork(getTextContent(element)); + } + else { + vCard.setEmailHome(getTextContent(element)); + } + } + } + + private void setupPhones() { + NodeList allPhones = document.getElementsByTagName("TEL"); + if (allPhones == null) return; + for (int i = 0; i < allPhones.getLength(); i++) { + NodeList nodes = allPhones.item(i).getChildNodes(); + String type = null; + String code = null; + String value = null; + for (int j = 0; j < nodes.getLength(); j++) { + Node node = nodes.item(j); + if (node.getNodeType() != Node.ELEMENT_NODE) continue; + String nodeName = node.getNodeName(); + if ("NUMBER".equals(nodeName)) { + value = getTextContent(node); + } + else if (isWorkHome(nodeName)) { + type = nodeName; + } + else { + code = nodeName; + } + } + if (code == null || value == null) continue; + if ("HOME".equals(type)) { + vCard.setPhoneHome(code, value); + } + else { // By default, setup work phone + vCard.setPhoneWork(code, value); + } + } + } + + private boolean isWorkHome(String nodeName) { + return "HOME".equals(nodeName) || "WORK".equals(nodeName); + } + + private void setupAddresses() { + NodeList allAddresses = document.getElementsByTagName("ADR"); + if (allAddresses == null) return; + for (int i = 0; i < allAddresses.getLength(); i++) { + Element addressNode = (Element) allAddresses.item(i); + + String type = null; + List<String> code = new ArrayList<String>(); + List<String> value = new ArrayList<String>(); + NodeList childNodes = addressNode.getChildNodes(); + for (int j = 0; j < childNodes.getLength(); j++) { + Node node = childNodes.item(j); + if (node.getNodeType() != Node.ELEMENT_NODE) continue; + String nodeName = node.getNodeName(); + if (isWorkHome(nodeName)) { + type = nodeName; + } + else { + code.add(nodeName); + value.add(getTextContent(node)); + } + } + for (int j = 0; j < value.size(); j++) { + if ("HOME".equals(type)) { + vCard.setAddressFieldHome((String) code.get(j), (String) value.get(j)); + } + else { // By default, setup work address + vCard.setAddressFieldWork((String) code.get(j), (String) value.get(j)); + } + } + } + } + + private String getTagContents(String tag) { + NodeList nodes = document.getElementsByTagName(tag); + if (nodes != null && nodes.getLength() == 1) { + return getTextContent(nodes.item(0)); + } + return null; + } + + private void setupSimpleFields() { + NodeList childNodes = document.getDocumentElement().getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node node = childNodes.item(i); + if (node instanceof Element) { + Element element = (Element) node; + + String field = element.getNodeName(); + if (element.getChildNodes().getLength() == 0) { + vCard.setField(field, ""); + } + else if (element.getChildNodes().getLength() == 1 && + element.getChildNodes().item(0) instanceof Text) { + vCard.setField(field, getTextContent(element)); + } + } + } + } + + private String getTextContent(Node node) { + StringBuilder result = new StringBuilder(); + appendText(result, node); + return result.toString(); + } + + private void appendText(StringBuilder result, Node node) { + NodeList childNodes = node.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node nd = childNodes.item(i); + String nodeValue = nd.getNodeValue(); + if (nodeValue != null) { + result.append(nodeValue); + } + appendText(result, nd); + } + } + } +} diff --git a/src/org/jivesoftware/smackx/provider/XHTMLExtensionProvider.java b/src/org/jivesoftware/smackx/provider/XHTMLExtensionProvider.java new file mode 100644 index 0000000..50f437f --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/XHTMLExtensionProvider.java @@ -0,0 +1,94 @@ +/** + * $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.smackx.provider; + +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.packet.XHTMLExtension; +import org.xmlpull.v1.XmlPullParser; + +/** + * The XHTMLExtensionProvider parses XHTML packets. + * + * @author Gaston Dombiak + */ +public class XHTMLExtensionProvider implements PacketExtensionProvider { + + /** + * Creates a new XHTMLExtensionProvider. + * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor + */ + public XHTMLExtensionProvider() { + } + + /** + * Parses a XHTMLExtension packet (extension sub-packet). + * + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public PacketExtension parseExtension(XmlPullParser parser) + throws Exception { + XHTMLExtension xhtmlExtension = new XHTMLExtension(); + boolean done = false; + StringBuilder buffer = new StringBuilder(); + int startDepth = parser.getDepth(); + int depth = parser.getDepth(); + String lastTag = ""; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("body")) { + buffer = new StringBuilder(); + depth = parser.getDepth(); + } + lastTag = parser.getText(); + buffer.append(parser.getText()); + } else if (eventType == XmlPullParser.TEXT) { + if (buffer != null) { + // We need to return valid XML so any inner text needs to be re-escaped + buffer.append(StringUtils.escapeForXML(parser.getText())); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("body") && parser.getDepth() <= depth) { + buffer.append(parser.getText()); + xhtmlExtension.addBody(buffer.toString()); + } + else if (parser.getName().equals(xhtmlExtension.getElementName()) + && parser.getDepth() <= startDepth) { + done = true; + } + else { + // This is a check for tags that are both a start and end tag like <br/> + // So that they aren't doubled + if(lastTag == null || !lastTag.equals(parser.getText())) { + buffer.append(parser.getText()); + } + } + } + } + + return xhtmlExtension; + } + +} diff --git a/src/org/jivesoftware/smackx/provider/package.html b/src/org/jivesoftware/smackx/provider/package.html new file mode 100644 index 0000000..962ba63 --- /dev/null +++ b/src/org/jivesoftware/smackx/provider/package.html @@ -0,0 +1 @@ +<body>Provides pluggable parsing logic for Smack extensions.</body>
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/pubsub/AccessModel.java b/src/org/jivesoftware/smackx/pubsub/AccessModel.java new file mode 100644 index 0000000..c1fa546 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/AccessModel.java @@ -0,0 +1,38 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+/**
+ * This enumeration represents the access models for the pubsub node
+ * as defined in the pubsub specification section <a href="http://xmpp.org/extensions/xep-0060.html#registrar-formtypes-config">16.4.3</a>
+ *
+ * @author Robin Collier
+ */
+public enum AccessModel
+{
+ /** Anyone may subscribe and retrieve items */
+ open,
+
+ /** Subscription request must be approved and only subscribers may retrieve items */
+ authorize,
+
+ /** Anyone with a presence subscription of both or from may subscribe and retrieve items */
+ presence,
+
+ /** Anyone in the specified roster group(s) may subscribe and retrieve items */
+ roster,
+
+ /** Only those on a whitelist may subscribe and retrieve items */
+ whitelist;
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/Affiliation.java b/src/org/jivesoftware/smackx/pubsub/Affiliation.java new file mode 100644 index 0000000..d55534d --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/Affiliation.java @@ -0,0 +1,111 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * Represents a affiliation between a user and a node, where the {@link #type} defines
+ * the type of affiliation.
+ *
+ * Affiliations are retrieved from the {@link PubSubManager#getAffiliations()} method, which
+ * gets affiliations for the calling user, based on the identity that is associated with
+ * the {@link Connection}.
+ *
+ * @author Robin Collier
+ */
+public class Affiliation implements PacketExtension
+{
+ protected String jid;
+ protected String node;
+ protected Type type;
+
+ public enum Type
+ {
+ member, none, outcast, owner, publisher
+ }
+
+ /**
+ * Constructs an affiliation.
+ *
+ * @param jid The JID with affiliation.
+ * @param affiliation The type of affiliation.
+ */
+ public Affiliation(String jid, Type affiliation)
+ {
+ this(jid, null, affiliation);
+ }
+
+ /**
+ * Constructs an affiliation.
+ *
+ * @param jid The JID with affiliation.
+ * @param node The node with affiliation.
+ * @param affiliation The type of affiliation.
+ */
+ public Affiliation(String jid, String node, Type affiliation)
+ {
+ this.jid = jid;
+ this.node = node;
+ type = affiliation;
+ }
+
+ public String getJid()
+ {
+ return jid;
+ }
+
+ public String getNode()
+ {
+ return node;
+ }
+
+ public Type getType()
+ {
+ return type;
+ }
+
+ public String getElementName()
+ {
+ return "affiliation";
+ }
+
+ public String getNamespace()
+ {
+ return null;
+ }
+
+ public String toXML()
+ {
+ StringBuilder builder = new StringBuilder("<");
+ builder.append(getElementName());
+ if (node != null)
+ appendAttribute(builder, "node", node);
+ appendAttribute(builder, "jid", jid);
+ appendAttribute(builder, "affiliation", type.toString());
+
+ builder.append("/>");
+ return builder.toString();
+ }
+
+ private void appendAttribute(StringBuilder builder, String att, String value)
+ {
+ builder.append(" ");
+ builder.append(att);
+ builder.append("='");
+ builder.append(value);
+ builder.append("'");
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/AffiliationsExtension.java b/src/org/jivesoftware/smackx/pubsub/AffiliationsExtension.java new file mode 100644 index 0000000..563147e --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/AffiliationsExtension.java @@ -0,0 +1,91 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents the <b>affiliations</b> element of the reply to a request for affiliations.
+ * It is defined in the specification in section <a href="http://xmpp.org/extensions/xep-0060.html#entity-affiliations">5.7 Retrieve Affiliations</a>.
+ *
+ * @author Robin Collier
+ */
+public class AffiliationsExtension extends NodeExtension
+{
+ protected List<Affiliation> items = Collections.EMPTY_LIST;
+
+ public AffiliationsExtension()
+ {
+ super(PubSubElementType.AFFILIATIONS);
+ }
+
+ public AffiliationsExtension(List<Affiliation> affiliationList)
+ {
+ super(PubSubElementType.AFFILIATIONS);
+
+ if (affiliationList != null)
+ items = affiliationList;
+ }
+
+ /**
+ * Affiliations for the specified node.
+ *
+ * @param nodeId
+ * @param subList
+ */
+ public AffiliationsExtension(String nodeId, List<Affiliation> affiliationList)
+ {
+ super(PubSubElementType.AFFILIATIONS, nodeId);
+
+ if (affiliationList != null)
+ items = affiliationList;
+ }
+
+ public List<Affiliation> getAffiliations()
+ {
+ return items;
+ }
+
+ @Override
+ public String toXML()
+ {
+ if ((items == null) || (items.size() == 0))
+ {
+ return super.toXML();
+ }
+ else
+ {
+ StringBuilder builder = new StringBuilder("<");
+ builder.append(getElementName());
+ if (getNode() != null)
+ {
+ builder.append(" node='");
+ builder.append(getNode());
+ builder.append("'");
+ }
+ builder.append(">");
+
+ for (Affiliation item : items)
+ {
+ builder.append(item.toXML());
+ }
+
+ builder.append("</");
+ builder.append(getElementName());
+ builder.append(">");
+ return builder.toString();
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/ChildrenAssociationPolicy.java b/src/org/jivesoftware/smackx/pubsub/ChildrenAssociationPolicy.java new file mode 100644 index 0000000..933a39e --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/ChildrenAssociationPolicy.java @@ -0,0 +1,32 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+/**
+ * This enumeration represents the children association policy for associating leaf nodes
+ * with collection nodes as defined in the pubsub specification section <a href="http://xmpp.org/extensions/xep-0060.html#registrar-formtypes-config">16.4.3</a>
+ *
+ * @author Robin Collier
+ */
+public enum ChildrenAssociationPolicy
+{
+ /** Anyone may associate leaf nodes with the collection */
+ all,
+
+ /** Only collection node owners may associate leaf nodes with the collection. */
+ owners,
+
+ /** Only those on a whitelist may associate leaf nodes with the collection. */
+ whitelist;
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/CollectionNode.java b/src/org/jivesoftware/smackx/pubsub/CollectionNode.java new file mode 100644 index 0000000..dcd1cc4 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/CollectionNode.java @@ -0,0 +1,31 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2009 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.smackx.pubsub;
+
+import org.jivesoftware.smack.Connection;
+
+public class CollectionNode extends Node
+{
+ CollectionNode(Connection connection, String nodeId)
+ {
+ super(connection, nodeId);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/ConfigurationEvent.java b/src/org/jivesoftware/smackx/pubsub/ConfigurationEvent.java new file mode 100644 index 0000000..67b8304 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/ConfigurationEvent.java @@ -0,0 +1,56 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * Represents the <b>configuration</b> element of a pubsub message event which
+ * associates a configuration form to the node which was configured. The form
+ * contains the current node configuration.
+ *
+ * @author Robin Collier
+ */
+public class ConfigurationEvent extends NodeExtension implements EmbeddedPacketExtension
+{
+ private ConfigureForm form;
+
+ public ConfigurationEvent(String nodeId)
+ {
+ super(PubSubElementType.CONFIGURATION, nodeId);
+ }
+
+ public ConfigurationEvent(String nodeId, ConfigureForm configForm)
+ {
+ super(PubSubElementType.CONFIGURATION, nodeId);
+ form = configForm;
+ }
+
+ public ConfigureForm getConfiguration()
+ {
+ return form;
+ }
+
+ public List<PacketExtension> getExtensions()
+ {
+ if (getConfiguration() == null)
+ return Collections.EMPTY_LIST;
+ else
+ return Arrays.asList(((PacketExtension)getConfiguration().getDataFormToSend()));
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/ConfigureForm.java b/src/org/jivesoftware/smackx/pubsub/ConfigureForm.java new file mode 100644 index 0000000..f6fe140 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/ConfigureForm.java @@ -0,0 +1,709 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.packet.DataForm;
+
+/**
+ * A decorator for a {@link Form} to easily enable reading and updating
+ * of node configuration. All operations read or update the underlying {@link DataForm}.
+ *
+ * <p>Unlike the {@link Form}.setAnswer(XXX)} methods, which throw an exception if the field does not
+ * exist, all <b>ConfigureForm.setXXX</b> methods will create the field in the wrapped form
+ * if it does not already exist.
+ *
+ * @author Robin Collier
+ */
+public class ConfigureForm extends Form
+{
+ /**
+ * Create a decorator from an existing {@link DataForm} that has been
+ * retrieved from parsing a node configuration request.
+ *
+ * @param configDataForm
+ */
+ public ConfigureForm(DataForm configDataForm)
+ {
+ super(configDataForm);
+ }
+
+ /**
+ * Create a decorator from an existing {@link Form} for node configuration.
+ * Typically, this can be used to create a decorator for an answer form
+ * by using the result of {@link #createAnswerForm()} as the input parameter.
+ *
+ * @param nodeConfigForm
+ */
+ public ConfigureForm(Form nodeConfigForm)
+ {
+ super(nodeConfigForm.getDataFormToSend());
+ }
+
+ /**
+ * Create a new form for configuring a node. This would typically only be used
+ * when creating and configuring a node at the same time via {@link PubSubManager#createNode(String, Form)}, since
+ * configuration of an existing node is typically accomplished by calling {@link LeafNode#getNodeConfiguration()} and
+ * using the resulting form to create a answer form. See {@link #ConfigureForm(Form)}.
+ * @param formType
+ */
+ public ConfigureForm(FormType formType)
+ {
+ super(formType.toString());
+ }
+
+ /**
+ * Get the currently configured {@link AccessModel}, null if it is not set.
+ *
+ * @return The current {@link AccessModel}
+ */
+ public AccessModel getAccessModel()
+ {
+ String value = getFieldValue(ConfigureNodeFields.access_model);
+
+ if (value == null)
+ return null;
+ else
+ return AccessModel.valueOf(value);
+ }
+
+ /**
+ * Sets the value of access model.
+ *
+ * @param accessModel
+ */
+ public void setAccessModel(AccessModel accessModel)
+ {
+ addField(ConfigureNodeFields.access_model, FormField.TYPE_LIST_SINGLE);
+ setAnswer(ConfigureNodeFields.access_model.getFieldName(), getListSingle(accessModel.toString()));
+ }
+
+ /**
+ * Returns the URL of an XSL transformation which can be applied to payloads in order to
+ * generate an appropriate message body element.
+ *
+ * @return URL to an XSL
+ */
+ public String getBodyXSLT()
+ {
+ return getFieldValue(ConfigureNodeFields.body_xslt);
+ }
+
+ /**
+ * Set the URL of an XSL transformation which can be applied to payloads in order to
+ * generate an appropriate message body element.
+ *
+ * @param bodyXslt The URL of an XSL
+ */
+ public void setBodyXSLT(String bodyXslt)
+ {
+ addField(ConfigureNodeFields.body_xslt, FormField.TYPE_TEXT_SINGLE);
+ setAnswer(ConfigureNodeFields.body_xslt.getFieldName(), bodyXslt);
+ }
+
+ /**
+ * The id's of the child nodes associated with a collection node (both leaf and collection).
+ *
+ * @return Iterator over the list of child nodes.
+ */
+ public Iterator<String> getChildren()
+ {
+ return getFieldValues(ConfigureNodeFields.children);
+ }
+
+ /**
+ * Set the list of child node ids that are associated with a collection node.
+ *
+ * @param children
+ */
+ public void setChildren(List<String> children)
+ {
+ addField(ConfigureNodeFields.children, FormField.TYPE_TEXT_MULTI);
+ setAnswer(ConfigureNodeFields.children.getFieldName(), children);
+ }
+
+ /**
+ * Returns the policy that determines who may associate children with the node.
+ *
+ * @return The current policy
+ */
+ public ChildrenAssociationPolicy getChildrenAssociationPolicy()
+ {
+ String value = getFieldValue(ConfigureNodeFields.children_association_policy);
+
+ if (value == null)
+ return null;
+ else
+ return ChildrenAssociationPolicy.valueOf(value);
+ }
+
+ /**
+ * Sets the policy that determines who may associate children with the node.
+ *
+ * @param policy The policy being set
+ */
+ public void setChildrenAssociationPolicy(ChildrenAssociationPolicy policy)
+ {
+ addField(ConfigureNodeFields.children_association_policy, FormField.TYPE_LIST_SINGLE);
+ List<String> values = new ArrayList<String>(1);
+ values.add(policy.toString());
+ setAnswer(ConfigureNodeFields.children_association_policy.getFieldName(), values);
+ }
+
+ /**
+ * Iterator of JID's that are on the whitelist that determines who can associate child nodes
+ * with the collection node. This is only relevant if {@link #getChildrenAssociationPolicy()} is set to
+ * {@link ChildrenAssociationPolicy#whitelist}.
+ *
+ * @return Iterator over whitelist
+ */
+ public Iterator<String> getChildrenAssociationWhitelist()
+ {
+ return getFieldValues(ConfigureNodeFields.children_association_whitelist);
+ }
+
+ /**
+ * Set the JID's in the whitelist of users that can associate child nodes with the collection
+ * node. This is only relevant if {@link #getChildrenAssociationPolicy()} is set to
+ * {@link ChildrenAssociationPolicy#whitelist}.
+ *
+ * @param whitelist The list of JID's
+ */
+ public void setChildrenAssociationWhitelist(List<String> whitelist)
+ {
+ addField(ConfigureNodeFields.children_association_whitelist, FormField.TYPE_JID_MULTI);
+ setAnswer(ConfigureNodeFields.children_association_whitelist.getFieldName(), whitelist);
+ }
+
+ /**
+ * Gets the maximum number of child nodes that can be associated with the collection node.
+ *
+ * @return The maximum number of child nodes
+ */
+ public int getChildrenMax()
+ {
+ return Integer.parseInt(getFieldValue(ConfigureNodeFields.children_max));
+ }
+
+ /**
+ * Set the maximum number of child nodes that can be associated with a collection node.
+ *
+ * @param max The maximum number of child nodes.
+ */
+ public void setChildrenMax(int max)
+ {
+ addField(ConfigureNodeFields.children_max, FormField.TYPE_TEXT_SINGLE);
+ setAnswer(ConfigureNodeFields.children_max.getFieldName(), max);
+ }
+
+ /**
+ * Gets the collection node which the node is affiliated with.
+ *
+ * @return The collection node id
+ */
+ public String getCollection()
+ {
+ return getFieldValue(ConfigureNodeFields.collection);
+ }
+
+ /**
+ * Sets the collection node which the node is affiliated with.
+ *
+ * @param collection The node id of the collection node
+ */
+ public void setCollection(String collection)
+ {
+ addField(ConfigureNodeFields.collection, FormField.TYPE_TEXT_SINGLE);
+ setAnswer(ConfigureNodeFields.collection.getFieldName(), collection);
+ }
+
+ /**
+ * Gets the URL of an XSL transformation which can be applied to the payload
+ * format in order to generate a valid Data Forms result that the client could
+ * display using a generic Data Forms rendering engine.
+ *
+ * @return The URL of an XSL transformation
+ */
+ public String getDataformXSLT()
+ {
+ return getFieldValue(ConfigureNodeFields.dataform_xslt);
+ }
+
+ /**
+ * Sets the URL of an XSL transformation which can be applied to the payload
+ * format in order to generate a valid Data Forms result that the client could
+ * display using a generic Data Forms rendering engine.
+ *
+ * @param url The URL of an XSL transformation
+ */
+ public void setDataformXSLT(String url)
+ {
+ addField(ConfigureNodeFields.dataform_xslt, FormField.TYPE_TEXT_SINGLE);
+ setAnswer(ConfigureNodeFields.dataform_xslt.getFieldName(), url);
+ }
+
+ /**
+ * Does the node deliver payloads with event notifications.
+ *
+ * @return true if it does, false otherwise
+ */
+ public boolean isDeliverPayloads()
+ {
+ return parseBoolean(getFieldValue(ConfigureNodeFields.deliver_payloads));
+ }
+
+ /**
+ * Sets whether the node will deliver payloads with event notifications.
+ *
+ * @param deliver true if the payload will be delivered, false otherwise
+ */
+ public void setDeliverPayloads(boolean deliver)
+ {
+ addField(ConfigureNodeFields.deliver_payloads, FormField.TYPE_BOOLEAN);
+ setAnswer(ConfigureNodeFields.deliver_payloads.getFieldName(), deliver);
+ }
+
+ /**
+ * Determines who should get replies to items
+ *
+ * @return Who should get the reply
+ */
+ public ItemReply getItemReply()
+ {
+ String value = getFieldValue(ConfigureNodeFields.itemreply);
+
+ if (value == null)
+ return null;
+ else
+ return ItemReply.valueOf(value);
+ }
+
+ /**
+ * Sets who should get the replies to items
+ *
+ * @param reply Defines who should get the reply
+ */
+ public void setItemReply(ItemReply reply)
+ {
+ addField(ConfigureNodeFields.itemreply, FormField.TYPE_LIST_SINGLE);
+ setAnswer(ConfigureNodeFields.itemreply.getFieldName(), getListSingle(reply.toString()));
+ }
+
+ /**
+ * Gets the maximum number of items to persisted to this node if {@link #isPersistItems()} is
+ * true.
+ *
+ * @return The maximum number of items to persist
+ */
+ public int getMaxItems()
+ {
+ return Integer.parseInt(getFieldValue(ConfigureNodeFields.max_items));
+ }
+
+ /**
+ * Set the maximum number of items to persisted to this node if {@link #isPersistItems()} is
+ * true.
+ *
+ * @param max The maximum number of items to persist
+ */
+ public void setMaxItems(int max)
+ {
+ addField(ConfigureNodeFields.max_items, FormField.TYPE_TEXT_SINGLE);
+ setAnswer(ConfigureNodeFields.max_items.getFieldName(), max);
+ }
+
+ /**
+ * Gets the maximum payload size in bytes.
+ *
+ * @return The maximum payload size
+ */
+ public int getMaxPayloadSize()
+ {
+ return Integer.parseInt(getFieldValue(ConfigureNodeFields.max_payload_size));
+ }
+
+ /**
+ * Sets the maximum payload size in bytes
+ *
+ * @param max The maximum payload size
+ */
+ public void setMaxPayloadSize(int max)
+ {
+ addField(ConfigureNodeFields.max_payload_size, FormField.TYPE_TEXT_SINGLE);
+ setAnswer(ConfigureNodeFields.max_payload_size.getFieldName(), max);
+ }
+
+ /**
+ * Gets the node type
+ *
+ * @return The node type
+ */
+ public NodeType getNodeType()
+ {
+ String value = getFieldValue(ConfigureNodeFields.node_type);
+
+ if (value == null)
+ return null;
+ else
+ return NodeType.valueOf(value);
+ }
+
+ /**
+ * Sets the node type
+ *
+ * @param type The node type
+ */
+ public void setNodeType(NodeType type)
+ {
+ addField(ConfigureNodeFields.node_type, FormField.TYPE_LIST_SINGLE);
+ setAnswer(ConfigureNodeFields.node_type.getFieldName(), getListSingle(type.toString()));
+ }
+
+ /**
+ * Determines if subscribers should be notified when the configuration changes.
+ *
+ * @return true if they should be notified, false otherwise
+ */
+ public boolean isNotifyConfig()
+ {
+ return parseBoolean(getFieldValue(ConfigureNodeFields.notify_config));
+ }
+
+ /**
+ * Sets whether subscribers should be notified when the configuration changes.
+ *
+ * @param notify true if subscribers should be notified, false otherwise
+ */
+ public void setNotifyConfig(boolean notify)
+ {
+ addField(ConfigureNodeFields.notify_config, FormField.TYPE_BOOLEAN);
+ setAnswer(ConfigureNodeFields.notify_config.getFieldName(), notify);
+ }
+
+ /**
+ * Determines whether subscribers should be notified when the node is deleted.
+ *
+ * @return true if subscribers should be notified, false otherwise
+ */
+ public boolean isNotifyDelete()
+ {
+ return parseBoolean(getFieldValue(ConfigureNodeFields.notify_delete));
+ }
+
+ /**
+ * Sets whether subscribers should be notified when the node is deleted.
+ *
+ * @param notify true if subscribers should be notified, false otherwise
+ */
+ public void setNotifyDelete(boolean notify)
+ {
+ addField(ConfigureNodeFields.notify_delete, FormField.TYPE_BOOLEAN);
+ setAnswer(ConfigureNodeFields.notify_delete.getFieldName(), notify);
+ }
+
+ /**
+ * Determines whether subscribers should be notified when items are deleted
+ * from the node.
+ *
+ * @return true if subscribers should be notified, false otherwise
+ */
+ public boolean isNotifyRetract()
+ {
+ return parseBoolean(getFieldValue(ConfigureNodeFields.notify_retract));
+ }
+
+ /**
+ * Sets whether subscribers should be notified when items are deleted
+ * from the node.
+ *
+ * @param notify true if subscribers should be notified, false otherwise
+ */
+ public void setNotifyRetract(boolean notify)
+ {
+ addField(ConfigureNodeFields.notify_retract, FormField.TYPE_BOOLEAN);
+ setAnswer(ConfigureNodeFields.notify_retract.getFieldName(), notify);
+ }
+
+ /**
+ * Determines whether items should be persisted in the node.
+ *
+ * @return true if items are persisted
+ */
+ public boolean isPersistItems()
+ {
+ return parseBoolean(getFieldValue(ConfigureNodeFields.persist_items));
+ }
+
+ /**
+ * Sets whether items should be persisted in the node.
+ *
+ * @param persist true if items should be persisted, false otherwise
+ */
+ public void setPersistentItems(boolean persist)
+ {
+ addField(ConfigureNodeFields.persist_items, FormField.TYPE_BOOLEAN);
+ setAnswer(ConfigureNodeFields.persist_items.getFieldName(), persist);
+ }
+
+ /**
+ * Determines whether to deliver notifications to available users only.
+ *
+ * @return true if users must be available
+ */
+ public boolean isPresenceBasedDelivery()
+ {
+ return parseBoolean(getFieldValue(ConfigureNodeFields.presence_based_delivery));
+ }
+
+ /**
+ * Sets whether to deliver notifications to available users only.
+ *
+ * @param presenceBased true if user must be available, false otherwise
+ */
+ public void setPresenceBasedDelivery(boolean presenceBased)
+ {
+ addField(ConfigureNodeFields.presence_based_delivery, FormField.TYPE_BOOLEAN);
+ setAnswer(ConfigureNodeFields.presence_based_delivery.getFieldName(), presenceBased);
+ }
+
+ /**
+ * Gets the publishing model for the node, which determines who may publish to it.
+ *
+ * @return The publishing model
+ */
+ public PublishModel getPublishModel()
+ {
+ String value = getFieldValue(ConfigureNodeFields.publish_model);
+
+ if (value == null)
+ return null;
+ else
+ return PublishModel.valueOf(value);
+ }
+
+ /**
+ * Sets the publishing model for the node, which determines who may publish to it.
+ *
+ * @param publish The enum representing the possible options for the publishing model
+ */
+ public void setPublishModel(PublishModel publish)
+ {
+ addField(ConfigureNodeFields.publish_model, FormField.TYPE_LIST_SINGLE);
+ setAnswer(ConfigureNodeFields.publish_model.getFieldName(), getListSingle(publish.toString()));
+ }
+
+ /**
+ * Iterator over the multi user chat rooms that are specified as reply rooms.
+ *
+ * @return The reply room JID's
+ */
+ public Iterator<String> getReplyRoom()
+ {
+ return getFieldValues(ConfigureNodeFields.replyroom);
+ }
+
+ /**
+ * Sets the multi user chat rooms that are specified as reply rooms.
+ *
+ * @param replyRooms The multi user chat room to use as reply rooms
+ */
+ public void setReplyRoom(List<String> replyRooms)
+ {
+ addField(ConfigureNodeFields.replyroom, FormField.TYPE_LIST_MULTI);
+ setAnswer(ConfigureNodeFields.replyroom.getFieldName(), replyRooms);
+ }
+
+ /**
+ * Gets the specific JID's for reply to.
+ *
+ * @return The JID's
+ */
+ public Iterator<String> getReplyTo()
+ {
+ return getFieldValues(ConfigureNodeFields.replyto);
+ }
+
+ /**
+ * Sets the specific JID's for reply to.
+ *
+ * @param replyTos The JID's to reply to
+ */
+ public void setReplyTo(List<String> replyTos)
+ {
+ addField(ConfigureNodeFields.replyto, FormField.TYPE_LIST_MULTI);
+ setAnswer(ConfigureNodeFields.replyto.getFieldName(), replyTos);
+ }
+
+ /**
+ * Gets the roster groups that are allowed to subscribe and retrieve items.
+ *
+ * @return The roster groups
+ */
+ public Iterator<String> getRosterGroupsAllowed()
+ {
+ return getFieldValues(ConfigureNodeFields.roster_groups_allowed);
+ }
+
+ /**
+ * Sets the roster groups that are allowed to subscribe and retrieve items.
+ *
+ * @param groups The roster groups
+ */
+ public void setRosterGroupsAllowed(List<String> groups)
+ {
+ addField(ConfigureNodeFields.roster_groups_allowed, FormField.TYPE_LIST_MULTI);
+ setAnswer(ConfigureNodeFields.roster_groups_allowed.getFieldName(), groups);
+ }
+
+ /**
+ * Determines if subscriptions are allowed.
+ *
+ * @return true if subscriptions are allowed, false otherwise
+ */
+ public boolean isSubscibe()
+ {
+ return parseBoolean(getFieldValue(ConfigureNodeFields.subscribe));
+ }
+
+ /**
+ * Sets whether subscriptions are allowed.
+ *
+ * @param subscribe true if they are, false otherwise
+ */
+ public void setSubscribe(boolean subscribe)
+ {
+ addField(ConfigureNodeFields.subscribe, FormField.TYPE_BOOLEAN);
+ setAnswer(ConfigureNodeFields.subscribe.getFieldName(), subscribe);
+ }
+
+ /**
+ * Gets the human readable node title.
+ *
+ * @return The node title
+ */
+ public String getTitle()
+ {
+ return getFieldValue(ConfigureNodeFields.title);
+ }
+
+ /**
+ * Sets a human readable title for the node.
+ *
+ * @param title The node title
+ */
+ public void setTitle(String title)
+ {
+ addField(ConfigureNodeFields.title, FormField.TYPE_TEXT_SINGLE);
+ setAnswer(ConfigureNodeFields.title.getFieldName(), title);
+ }
+
+ /**
+ * The type of node data, usually specified by the namespace of the payload (if any).
+ *
+ * @return The type of node data
+ */
+ public String getDataType()
+ {
+ return getFieldValue(ConfigureNodeFields.type);
+ }
+
+ /**
+ * Sets the type of node data, usually specified by the namespace of the payload (if any).
+ *
+ * @param type The type of node data
+ */
+ public void setDataType(String type)
+ {
+ addField(ConfigureNodeFields.type, FormField.TYPE_TEXT_SINGLE);
+ setAnswer(ConfigureNodeFields.type.getFieldName(), type);
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder result = new StringBuilder(getClass().getName() + " Content [");
+
+ Iterator<FormField> fields = getFields();
+
+ while (fields.hasNext())
+ {
+ FormField formField = fields.next();
+ result.append('(');
+ result.append(formField.getVariable());
+ result.append(':');
+
+ Iterator<String> values = formField.getValues();
+ StringBuilder valuesBuilder = new StringBuilder();
+
+ while (values.hasNext())
+ {
+ if (valuesBuilder.length() > 0)
+ result.append(',');
+ String value = (String)values.next();
+ valuesBuilder.append(value);
+ }
+
+ if (valuesBuilder.length() == 0)
+ valuesBuilder.append("NOT SET");
+ result.append(valuesBuilder);
+ result.append(')');
+ }
+ result.append(']');
+ return result.toString();
+ }
+
+ static private boolean parseBoolean(String fieldValue)
+ {
+ return ("1".equals(fieldValue) || "true".equals(fieldValue));
+ }
+
+ private String getFieldValue(ConfigureNodeFields field)
+ {
+ FormField formField = getField(field.getFieldName());
+
+ return (formField.getValues().hasNext()) ? formField.getValues().next() : null;
+ }
+
+ private Iterator<String> getFieldValues(ConfigureNodeFields field)
+ {
+ FormField formField = getField(field.getFieldName());
+
+ return formField.getValues();
+ }
+
+ private void addField(ConfigureNodeFields nodeField, String type)
+ {
+ String fieldName = nodeField.getFieldName();
+
+ if (getField(fieldName) == null)
+ {
+ FormField field = new FormField(fieldName);
+ field.setType(type);
+ addField(field);
+ }
+ }
+
+ private List<String> getListSingle(String value)
+ {
+ List<String> list = new ArrayList<String>(1);
+ list.add(value);
+ return list;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/ConfigureNodeFields.java b/src/org/jivesoftware/smackx/pubsub/ConfigureNodeFields.java new file mode 100644 index 0000000..3912483 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/ConfigureNodeFields.java @@ -0,0 +1,218 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.net.URL;
+
+import org.jivesoftware.smackx.Form;
+
+/**
+ * This enumeration represents all the fields of a node configuration form. This enumeration
+ * is not required when using the {@link ConfigureForm} to configure nodes, but may be helpful
+ * for generic UI's using only a {@link Form} for configuration.
+ *
+ * @author Robin Collier
+ */
+public enum ConfigureNodeFields
+{
+ /**
+ * Determines who may subscribe and retrieve items
+ *
+ * <p><b>Value: {@link AccessModel}</b></p>
+ */
+ access_model,
+
+ /**
+ * The URL of an XSL transformation which can be applied to
+ * payloads in order to generate an appropriate message
+ * body element
+ *
+ * <p><b>Value: {@link URL}</b></p>
+ */
+ body_xslt,
+
+ /**
+ * The collection with which a node is affiliated
+ *
+ * <p><b>Value: String</b></p>
+ */
+ collection,
+
+ /**
+ * The URL of an XSL transformation which can be applied to
+ * payload format in order to generate a valid Data Forms result
+ * that the client could display using a generic Data Forms
+ * rendering engine body element.
+ *
+ * <p><b>Value: {@link URL}</b></p>
+ */
+ dataform_xslt,
+
+ /**
+ * Whether to deliver payloads with event notifications
+ *
+ * <p><b>Value: boolean</b></p>
+ */
+ deliver_payloads,
+
+ /**
+ * Whether owners or publisher should receive replies to items
+ *
+ * <p><b>Value: {@link ItemReply}</b></p>
+ */
+ itemreply,
+
+ /**
+ * Who may associate leaf nodes with a collection
+ *
+ * <p><b>Value: {@link ChildrenAssociationPolicy}</b></p>
+ */
+ children_association_policy,
+
+ /**
+ * The list of JIDs that may associate leaf nodes with a
+ * collection
+ *
+ * <p><b>Value: List of JIDs as Strings</b></p>
+ */
+ children_association_whitelist,
+
+ /**
+ * The child nodes (leaf or collection) associated with a collection
+ *
+ * <p><b>Value: List of Strings</b></p>
+ */
+ children,
+
+ /**
+ * The maximum number of child nodes that can be associated with a
+ * collection
+ *
+ * <p><b>Value: int</b></p>
+ */
+ children_max,
+
+ /**
+ * The maximum number of items to persist
+ *
+ * <p><b>Value: int</b></p>
+ */
+ max_items,
+
+ /**
+ * The maximum payload size in bytes
+ *
+ * <p><b>Value: int</b></p>
+ */
+ max_payload_size,
+
+ /**
+ * Whether the node is a leaf (default) or collection
+ *
+ * <p><b>Value: {@link NodeType}</b></p>
+ */
+ node_type,
+
+ /**
+ * Whether to notify subscribers when the node configuration changes
+ *
+ * <p><b>Value: boolean</b></p>
+ */
+ notify_config,
+
+ /**
+ * Whether to notify subscribers when the node is deleted
+ *
+ * <p><b>Value: boolean</b></p>
+ */
+ notify_delete,
+
+ /**
+ * Whether to notify subscribers when items are removed from the node
+ *
+ * <p><b>Value: boolean</b></p>
+ */
+ notify_retract,
+
+ /**
+ * Whether to persist items to storage. This is required to have multiple
+ * items in the node.
+ *
+ * <p><b>Value: boolean</b></p>
+ */
+ persist_items,
+
+ /**
+ * Whether to deliver notifications to available users only
+ *
+ * <p><b>Value: boolean</b></p>
+ */
+ presence_based_delivery,
+
+ /**
+ * Defines who can publish to the node
+ *
+ * <p><b>Value: {@link PublishModel}</b></p>
+ */
+ publish_model,
+
+ /**
+ * The specific multi-user chat rooms to specify for replyroom
+ *
+ * <p><b>Value: List of JIDs as Strings</b></p>
+ */
+ replyroom,
+
+ /**
+ * The specific JID(s) to specify for replyto
+ *
+ * <p><b>Value: List of JIDs as Strings</b></p>
+ */
+ replyto,
+
+ /**
+ * The roster group(s) allowed to subscribe and retrieve items
+ *
+ * <p><b>Value: List of strings</b></p>
+ */
+ roster_groups_allowed,
+
+ /**
+ * Whether to allow subscriptions
+ *
+ * <p><b>Value: boolean</b></p>
+ */
+ subscribe,
+
+ /**
+ * A friendly name for the node
+ *
+ * <p><b>Value: String</b></p>
+ */
+ title,
+
+ /**
+ * The type of node data, ussually specified by the namespace
+ * of the payload(if any);MAY be a list-single rather than a
+ * text single
+ *
+ * <p><b>Value: String</b></p>
+ */
+ type;
+
+ public String getFieldName()
+ {
+ return "pubsub#" + toString();
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/EmbeddedPacketExtension.java b/src/org/jivesoftware/smackx/pubsub/EmbeddedPacketExtension.java new file mode 100644 index 0000000..b17a66a --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/EmbeddedPacketExtension.java @@ -0,0 +1,45 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.List;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.util.PacketParserUtils;
+
+/**
+ * This interface defines {@link PacketExtension} implementations that contain other
+ * extensions. This effectively extends the idea of an extension within one of the
+ * top level {@link Packet} types to consider any embedded element to be an extension
+ * of its parent. This more easily enables the usage of some of Smacks parsing
+ * utilities such as {@link PacketParserUtils#parsePacketExtension(String, String, org.xmlpull.v1.XmlPullParser)} to be used
+ * to parse any element of the XML being parsed.
+ *
+ * <p>Top level extensions have only one element, but they can have multiple children, or
+ * their children can have multiple children. This interface is a way of allowing extensions
+ * to be embedded within one another as a partial or complete one to one mapping of extension
+ * to element.
+ *
+ * @author Robin Collier
+ */
+public interface EmbeddedPacketExtension extends PacketExtension
+{
+ /**
+ * Get the list of embedded {@link PacketExtension} objects.
+ *
+ * @return List of embedded {@link PacketExtension}
+ */
+ List<PacketExtension> getExtensions();
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/EventElement.java b/src/org/jivesoftware/smackx/pubsub/EventElement.java new file mode 100644 index 0000000..165970f --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/EventElement.java @@ -0,0 +1,74 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
+
+/**
+ * Represents the top level element of a pubsub event extension. All types of pubsub events are
+ * represented by this class. The specific type can be found by {@link #getEventType()}. The
+ * embedded event information, which is specific to the event type, can be retrieved by the {@link #getEvent()}
+ * method.
+ *
+ * @author Robin Collier
+ */
+public class EventElement implements EmbeddedPacketExtension
+{
+ private EventElementType type;
+ private NodeExtension ext;
+
+ public EventElement(EventElementType eventType, NodeExtension eventExt)
+ {
+ type = eventType;
+ ext = eventExt;
+ }
+
+ public EventElementType getEventType()
+ {
+ return type;
+ }
+
+ public List<PacketExtension> getExtensions()
+ {
+ return Arrays.asList(new PacketExtension[]{getEvent()});
+ }
+
+ public NodeExtension getEvent()
+ {
+ return ext;
+ }
+
+ public String getElementName()
+ {
+ return "event";
+ }
+
+ public String getNamespace()
+ {
+ return PubSubNamespace.EVENT.getXmlns();
+ }
+
+ public String toXML()
+ {
+ StringBuilder builder = new StringBuilder("<event xmlns='" + PubSubNamespace.EVENT.getXmlns() + "'>");
+
+ builder.append(ext.toXML());
+ builder.append("</event>");
+ return builder.toString();
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/EventElementType.java b/src/org/jivesoftware/smackx/pubsub/EventElementType.java new file mode 100644 index 0000000..343edbe --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/EventElementType.java @@ -0,0 +1,41 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+/**
+ * This enumeration defines the possible event types that are supported within pubsub
+ * event messages.
+ *
+ * @author Robin Collier
+ */
+public enum EventElementType
+{
+ /** A node has been associated or dissassociated with a collection node */
+ collection,
+
+ /** A node has had its configuration changed */
+ configuration,
+
+ /** A node has been deleted */
+ delete,
+
+ /** Items have been published to a node */
+ items,
+
+ /** All items have been purged from a node */
+ purge,
+
+ /** A node has been subscribed to */
+ subscription
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/FormNode.java b/src/org/jivesoftware/smackx/pubsub/FormNode.java new file mode 100644 index 0000000..e08bed2 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/FormNode.java @@ -0,0 +1,99 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import org.jivesoftware.smackx.Form;
+
+/**
+ * Generic packet extension which represents any pubsub form that is
+ * parsed from the incoming stream or being sent out to the server.
+ *
+ * Form types are defined in {@link FormNodeType}.
+ *
+ * @author Robin Collier
+ */
+public class FormNode extends NodeExtension
+{
+ private Form configForm;
+
+ /**
+ * Create a {@link FormNode} which contains the specified form.
+ *
+ * @param formType The type of form being sent
+ * @param submitForm The form
+ */
+ public FormNode(FormNodeType formType, Form submitForm)
+ {
+ super(formType.getNodeElement());
+
+ if (submitForm == null)
+ throw new IllegalArgumentException("Submit form cannot be null");
+ configForm = submitForm;
+ }
+
+ /**
+ * Create a {@link FormNode} which contains the specified form, which is
+ * associated with the specified node.
+ *
+ * @param formType The type of form being sent
+ * @param nodeId The node the form is associated with
+ * @param submitForm The form
+ */
+ public FormNode(FormNodeType formType, String nodeId, Form submitForm)
+ {
+ super(formType.getNodeElement(), nodeId);
+
+ if (submitForm == null)
+ throw new IllegalArgumentException("Submit form cannot be null");
+ configForm = submitForm;
+ }
+
+ /**
+ * Get the Form that is to be sent, or was retrieved from the server.
+ *
+ * @return The form
+ */
+ public Form getForm()
+ {
+ return configForm;
+ }
+
+ @Override
+ public String toXML()
+ {
+ if (configForm == null)
+ {
+ return super.toXML();
+ }
+ else
+ {
+ StringBuilder builder = new StringBuilder("<");
+ builder.append(getElementName());
+
+ if (getNode() != null)
+ {
+ builder.append(" node='");
+ builder.append(getNode());
+ builder.append("'>");
+ }
+ else
+ builder.append('>');
+ builder.append(configForm.getDataFormToSend().toXML());
+ builder.append("</");
+ builder.append(getElementName() + '>');
+ return builder.toString();
+ }
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/FormNodeType.java b/src/org/jivesoftware/smackx/pubsub/FormNodeType.java new file mode 100644 index 0000000..6a163ee --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/FormNodeType.java @@ -0,0 +1,50 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
+
+/**
+ * The types of forms supported by the pubsub specification.
+ *
+ * @author Robin Collier
+ */
+public enum FormNodeType
+{
+ /** Form for configuring an existing node */
+ CONFIGURE_OWNER,
+
+ /** Form for configuring a node during creation */
+ CONFIGURE,
+
+ /** Form for configuring subscription options */
+ OPTIONS,
+
+ /** Form which represents the default node configuration options */
+ DEFAULT;
+
+ public PubSubElementType getNodeElement()
+ {
+ return PubSubElementType.valueOf(toString());
+ }
+
+ public static FormNodeType valueOfFromElementName(String elem, String configNamespace)
+ {
+ if ("configure".equals(elem) && PubSubNamespace.OWNER.getXmlns().equals(configNamespace))
+ {
+ return CONFIGURE_OWNER;
+ }
+ return valueOf(elem.toUpperCase());
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/FormType.java b/src/org/jivesoftware/smackx/pubsub/FormType.java new file mode 100644 index 0000000..e0fff51 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/FormType.java @@ -0,0 +1,26 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import org.jivesoftware.smackx.Form;
+
+/**
+ * Defines the allowable types for a {@link Form}
+ *
+ * @author Robin Collier
+ */
+public enum FormType
+{
+ form, submit, cancel, result;
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/pubsub/GetItemsRequest.java b/src/org/jivesoftware/smackx/pubsub/GetItemsRequest.java new file mode 100644 index 0000000..341b7b5 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/GetItemsRequest.java @@ -0,0 +1,85 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.pubsub; + +/** + * Represents a request to subscribe to a node. + * + * @author Robin Collier + */ +public class GetItemsRequest extends NodeExtension +{ + protected String subId; + protected int maxItems; + + public GetItemsRequest(String nodeId) + { + super(PubSubElementType.ITEMS, nodeId); + } + + public GetItemsRequest(String nodeId, String subscriptionId) + { + super(PubSubElementType.ITEMS, nodeId); + subId = subscriptionId; + } + + public GetItemsRequest(String nodeId, int maxItemsToReturn) + { + super(PubSubElementType.ITEMS, nodeId); + maxItems = maxItemsToReturn; + } + + public GetItemsRequest(String nodeId, String subscriptionId, int maxItemsToReturn) + { + this(nodeId, maxItemsToReturn); + subId = subscriptionId; + } + + public String getSubscriptionId() + { + return subId; + } + + public int getMaxItems() + { + return maxItems; + } + + @Override + public String toXML() + { + StringBuilder builder = new StringBuilder("<"); + builder.append(getElementName()); + + builder.append(" node='"); + builder.append(getNode()); + builder.append("'"); + + if (getSubscriptionId() != null) + { + builder.append(" subid='"); + builder.append(getSubscriptionId()); + builder.append("'"); + } + + if (getMaxItems() > 0) + { + builder.append(" max_items='"); + builder.append(getMaxItems()); + builder.append("'"); + } + builder.append("/>"); + return builder.toString(); + } +} diff --git a/src/org/jivesoftware/smackx/pubsub/Item.java b/src/org/jivesoftware/smackx/pubsub/Item.java new file mode 100644 index 0000000..2ce0baa --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/Item.java @@ -0,0 +1,132 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smackx.pubsub.provider.ItemProvider;
+
+/**
+ * This class represents an item that has been, or will be published to a
+ * pubsub node. An <tt>Item</tt> has several properties that are dependent
+ * on the configuration of the node to which it has been or will be published.
+ *
+ * <h1>An Item received from a node (via {@link LeafNode#getItems()} or {@link LeafNode#addItemEventListener(org.jivesoftware.smackx.pubsub.listener.ItemEventListener)}</b>
+ * <li>Will always have an id (either user or server generated) unless node configuration has both
+ * {@link ConfigureForm#isPersistItems()} and {@link ConfigureForm#isDeliverPayloads()}set to false.
+ * <li>Will have a payload if the node configuration has {@link ConfigureForm#isDeliverPayloads()} set
+ * to true, otherwise it will be null.
+ *
+ * <h1>An Item created to send to a node (via {@link LeafNode#send()} or {@link LeafNode#publish()}</b>
+ * <li>The id is optional, since the server will generate one if necessary, but should be used if it is
+ * meaningful in the context of the node. This value must be unique within the node that it is sent to, since
+ * resending an item with the same id will overwrite the one that already exists if the items are persisted.
+ * <li>Will require payload if the node configuration has {@link ConfigureForm#isDeliverPayloads()} set
+ * to true.
+ *
+ * <p>To customise the payload object being returned from the {@link #getPayload()} method, you can
+ * add a custom parser as explained in {@link ItemProvider}.
+ *
+ * @author Robin Collier
+ */
+public class Item extends NodeExtension
+{
+ private String id;
+
+ /**
+ * Create an empty <tt>Item</tt> with no id. This is a valid item for nodes which are configured
+ * so that {@link ConfigureForm#isDeliverPayloads()} is false. In most cases an id will be generated by the server.
+ * For nodes configured with {@link ConfigureForm#isDeliverPayloads()} and {@link ConfigureForm#isPersistItems()}
+ * set to false, no <tt>Item</tt> is sent to the node, you have to use {@link LeafNode#send()} or {@link LeafNode#publish()}
+ * methods in this case.
+ */
+ public Item()
+ {
+ super(PubSubElementType.ITEM);
+ }
+
+ /**
+ * Create an <tt>Item</tt> with an id but no payload. This is a valid item for nodes which are configured
+ * so that {@link ConfigureForm#isDeliverPayloads()} is false.
+ *
+ * @param itemId The id if the item. It must be unique within the node unless overwriting and existing item.
+ * Passing null is the equivalent of calling {@link #Item()}.
+ */
+ public Item(String itemId)
+ {
+ // The element type is actually irrelevant since we override getNamespace() to return null
+ super(PubSubElementType.ITEM);
+ id = itemId;
+ }
+
+ /**
+ * Create an <tt>Item</tt> with an id and a node id.
+ * <p>
+ * <b>Note:</b> This is not valid for publishing an item to a node, only receiving from
+ * one as part of {@link Message}. If used to create an Item to publish
+ * (via {@link LeafNode#publish(Item)}, the server <i>may</i> return an
+ * error for an invalid packet.
+ *
+ * @param itemId The id of the item.
+ * @param nodeId The id of the node which the item was published to.
+ */
+ public Item(String itemId, String nodeId)
+ {
+ super(PubSubElementType.ITEM_EVENT, nodeId);
+ id = itemId;
+ }
+
+ /**
+ * Get the item id. Unique to the node it is associated with.
+ *
+ * @return The id
+ */
+ public String getId()
+ {
+ return id;
+ }
+
+ @Override
+ public String getNamespace()
+ {
+ return null;
+ }
+
+ @Override
+ public String toXML()
+ {
+ StringBuilder builder = new StringBuilder("<item");
+
+ if (id != null)
+ {
+ builder.append(" id='");
+ builder.append(id);
+ builder.append("'");
+ }
+
+ if (getNode() != null) {
+ builder.append(" node='");
+ builder.append(getNode());
+ builder.append("'");
+ }
+ builder.append("/>");
+
+ return builder.toString();
+ }
+
+ @Override
+ public String toString()
+ {
+ return getClass().getName() + " | Content [" + toXML() + "]";
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/ItemDeleteEvent.java b/src/org/jivesoftware/smackx/pubsub/ItemDeleteEvent.java new file mode 100644 index 0000000..82ab7df --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/ItemDeleteEvent.java @@ -0,0 +1,62 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents an event in which items have been deleted from the node.
+ *
+ * @author Robin Collier
+ */
+public class ItemDeleteEvent extends SubscriptionEvent
+{
+ private List<String> itemIds = Collections.EMPTY_LIST;
+
+ /**
+ * Constructs an <tt>ItemDeleteEvent</tt> that indicates the the supplied
+ * items (by id) have been deleted, and that the event matches the listed
+ * subscriptions. The subscriptions would have been created by calling
+ * {@link LeafNode#subscribe(String)}.
+ *
+ * @param nodeId The id of the node the event came from
+ * @param deletedItemIds The item ids of the items that were deleted.
+ * @param subscriptionIds The subscriptions that match the event.
+ */
+ public ItemDeleteEvent(String nodeId, List<String> deletedItemIds, List<String> subscriptionIds)
+ {
+ super(nodeId, subscriptionIds);
+
+ if (deletedItemIds == null)
+ throw new IllegalArgumentException("deletedItemIds cannot be null");
+ itemIds = deletedItemIds;
+ }
+
+ /**
+ * Get the item id's of the items that have been deleted.
+ *
+ * @return List of item id's
+ */
+ public List<String> getItemIds()
+ {
+ return Collections.unmodifiableList(itemIds);
+ }
+
+ @Override
+ public String toString()
+ {
+ return getClass().getName() + " [subscriptions: " + getSubscriptions() + "], [Deleted Items: " + itemIds + ']';
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/ItemPublishEvent.java b/src/org/jivesoftware/smackx/pubsub/ItemPublishEvent.java new file mode 100644 index 0000000..1ef1f67 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/ItemPublishEvent.java @@ -0,0 +1,123 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Represents an event generated by an item(s) being published to a node.
+ *
+ * @author Robin Collier
+ */
+public class ItemPublishEvent <T extends Item> extends SubscriptionEvent
+{
+ private List<T> items;
+ private Date originalDate;
+
+ /**
+ * Constructs an <tt>ItemPublishEvent</tt> with the provided list
+ * of {@link Item} that were published.
+ *
+ * @param nodeId The id of the node the event came from
+ * @param eventItems The list of {@link Item} that were published
+ */
+ public ItemPublishEvent(String nodeId, List<T> eventItems)
+ {
+ super(nodeId);
+ items = eventItems;
+ }
+
+ /**
+ * Constructs an <tt>ItemPublishEvent</tt> with the provided list
+ * of {@link Item} that were published. The list of subscription ids
+ * represents the subscriptions that matched the event, in the case
+ * of the user having multiple subscriptions.
+ *
+ * @param nodeId The id of the node the event came from
+ * @param eventItems The list of {@link Item} that were published
+ * @param subscriptionIds The list of subscriptionIds
+ */
+ public ItemPublishEvent(String nodeId, List<T> eventItems, List<String> subscriptionIds)
+ {
+ super(nodeId, subscriptionIds);
+ items = eventItems;
+ }
+
+ /**
+ * Constructs an <tt>ItemPublishEvent</tt> with the provided list
+ * of {@link Item} that were published in the past. The published
+ * date signifies that this is delayed event. The list of subscription ids
+ * represents the subscriptions that matched the event, in the case
+ * of the user having multiple subscriptions.
+ *
+ * @param nodeId The id of the node the event came from
+ * @param eventItems The list of {@link Item} that were published
+ * @param subscriptionIds The list of subscriptionIds
+ * @param publishedDate The original publishing date of the events
+ */
+ public ItemPublishEvent(String nodeId, List<T> eventItems, List<String> subscriptionIds, Date publishedDate)
+ {
+ super(nodeId, subscriptionIds);
+ items = eventItems;
+
+ if (publishedDate != null)
+ originalDate = publishedDate;
+ }
+
+ /**
+ * Get the list of {@link Item} that were published.
+ *
+ * @return The list of published {@link Item}
+ */
+ public List<T> getItems()
+ {
+ return Collections.unmodifiableList(items);
+ }
+
+ /**
+ * Indicates whether this event was delayed. That is, the items
+ * were published to the node at some time in the past. This will
+ * typically happen if there is an item already published to the node
+ * before a user subscribes to it. In this case, when the user
+ * subscribes, the server may send the last item published to the node
+ * with a delay date showing its time of original publication.
+ *
+ * @return true if the items are delayed, false otherwise.
+ */
+ public boolean isDelayed()
+ {
+ return (originalDate != null);
+ }
+
+ /**
+ * Gets the original date the items were published. This is only
+ * valid if {@link #isDelayed()} is true.
+ *
+ * @return Date items were published if {@link #isDelayed()} is true, null otherwise.
+ */
+ public Date getPublishedDate()
+ {
+ return originalDate;
+ }
+
+ @Override
+ public String toString()
+ {
+ return getClass().getName() + " [subscriptions: " + getSubscriptions() + "], [Delayed: " +
+ (isDelayed() ? originalDate.toString() : "false") + ']';
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/ItemReply.java b/src/org/jivesoftware/smackx/pubsub/ItemReply.java new file mode 100644 index 0000000..3e090d9 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/ItemReply.java @@ -0,0 +1,29 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+/**
+ * These are the options for the node configuration setting {@link ConfigureForm#setItemReply(ItemReply)},
+ * which defines who should receive replies to items.
+ *
+ * @author Robin Collier
+ */
+public enum ItemReply
+{
+ /** The node owner */
+ owner,
+
+ /** The item publisher */
+ publisher;
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/ItemsExtension.java b/src/org/jivesoftware/smackx/pubsub/ItemsExtension.java new file mode 100644 index 0000000..c98d93a --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/ItemsExtension.java @@ -0,0 +1,196 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.List;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * This class is used to for multiple purposes.
+ * <li>It can represent an event containing a list of items that have been published
+ * <li>It can represent an event containing a list of retracted (deleted) items.
+ * <li>It can represent a request to delete a list of items.
+ * <li>It can represent a request to get existing items.
+ *
+ * <p><b>Please note, this class is used for internal purposes, and is not required for usage of
+ * pubsub functionality.</b>
+ *
+ * @author Robin Collier
+ */
+public class ItemsExtension extends NodeExtension implements EmbeddedPacketExtension
+{
+ protected ItemsElementType type;
+ protected Boolean notify;
+ protected List<? extends PacketExtension> items;
+
+ public enum ItemsElementType
+ {
+ /** An items element, which has an optional <b>max_items</b> attribute when requesting items */
+ items(PubSubElementType.ITEMS, "max_items"),
+
+ /** A retract element, which has an optional <b>notify</b> attribute when publishing deletions */
+ retract(PubSubElementType.RETRACT, "notify");
+
+ private PubSubElementType elem;
+ private String att;
+
+ private ItemsElementType(PubSubElementType nodeElement, String attribute)
+ {
+ elem = nodeElement;
+ att = attribute;
+ }
+
+ public PubSubElementType getNodeElement()
+ {
+ return elem;
+ }
+
+ public String getElementAttribute()
+ {
+ return att;
+ }
+ }
+
+ /**
+ * Construct an instance with a list representing items that have been published or deleted.
+ *
+ * <p>Valid scenarios are:
+ * <li>Request items from node - itemsType = {@link ItemsElementType#items}, items = list of {@link Item} and an
+ * optional value for the <b>max_items</b> attribute.
+ * <li>Request to delete items - itemsType = {@link ItemsElementType#retract}, items = list of {@link Item} containing
+ * only id's and an optional value for the <b>notify</b> attribute.
+ * <li>Items published event - itemsType = {@link ItemsElementType#items}, items = list of {@link Item} and
+ * attributeValue = <code>null</code>
+ * <li>Items deleted event - itemsType = {@link ItemsElementType#items}, items = list of {@link RetractItem} and
+ * attributeValue = <code>null</code>
+ *
+ * @param itemsType Type of representation
+ * @param nodeId The node to which the items are being sent or deleted
+ * @param items The list of {@link Item} or {@link RetractItem}
+ * @param attributeValue The value of the <b>max_items</b>
+ */
+ public ItemsExtension(ItemsElementType itemsType, String nodeId, List<? extends PacketExtension> items)
+ {
+ super(itemsType.getNodeElement(), nodeId);
+ type = itemsType;
+ this.items = items;
+ }
+
+ /**
+ * Construct an instance with a list representing items that have been published or deleted.
+ *
+ * <p>Valid scenarios are:
+ * <li>Request items from node - itemsType = {@link ItemsElementType#items}, items = list of {@link Item} and an
+ * optional value for the <b>max_items</b> attribute.
+ * <li>Request to delete items - itemsType = {@link ItemsElementType#retract}, items = list of {@link Item} containing
+ * only id's and an optional value for the <b>notify</b> attribute.
+ * <li>Items published event - itemsType = {@link ItemsElementType#items}, items = list of {@link Item} and
+ * attributeValue = <code>null</code>
+ * <li>Items deleted event - itemsType = {@link ItemsElementType#items}, items = list of {@link RetractItem} and
+ * attributeValue = <code>null</code>
+ *
+ * @param itemsType Type of representation
+ * @param nodeId The node to which the items are being sent or deleted
+ * @param items The list of {@link Item} or {@link RetractItem}
+ * @param attributeValue The value of the <b>max_items</b>
+ */
+ public ItemsExtension(String nodeId, List<? extends PacketExtension> items, boolean notify)
+ {
+ super(ItemsElementType.retract.getNodeElement(), nodeId);
+ type = ItemsElementType.retract;
+ this.items = items;
+ this.notify = notify;
+ }
+
+ /**
+ * Get the type of element
+ *
+ * @return The element type
+ */
+ public ItemsElementType getItemsElementType()
+ {
+ return type;
+ }
+
+ public List<PacketExtension> getExtensions()
+ {
+ return (List<PacketExtension>)getItems();
+ }
+
+ /**
+ * Gets the items related to the type of request or event.
+ *
+ * return List of {@link Item}, {@link RetractItem}, or null
+ */
+ public List<? extends PacketExtension> getItems()
+ {
+ return items;
+ }
+
+ /**
+ * Gets the value of the optional attribute related to the {@link ItemsElementType}.
+ *
+ * @return The attribute value
+ */
+ public boolean getNotify()
+ {
+ return notify;
+ }
+
+ @Override
+ public String toXML()
+ {
+ if ((items == null) || (items.size() == 0))
+ {
+ return super.toXML();
+ }
+ else
+ {
+ StringBuilder builder = new StringBuilder("<");
+ builder.append(getElementName());
+ builder.append(" node='");
+ builder.append(getNode());
+
+ if (notify != null)
+ {
+ builder.append("' ");
+ builder.append(type.getElementAttribute());
+ builder.append("='");
+ builder.append(notify.equals(Boolean.TRUE) ? 1 : 0);
+ builder.append("'>");
+ }
+ else
+ {
+ builder.append("'>");
+ for (PacketExtension item : items)
+ {
+ builder.append(item.toXML());
+ }
+ }
+
+ builder.append("</");
+ builder.append(getElementName());
+ builder.append(">");
+ return builder.toString();
+ }
+ }
+
+ @Override
+ public String toString()
+ {
+ return getClass().getName() + "Content [" + toXML() + "]";
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/LeafNode.java b/src/org/jivesoftware/smackx/pubsub/LeafNode.java new file mode 100644 index 0000000..eee6293 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/LeafNode.java @@ -0,0 +1,352 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ.Type;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+import org.jivesoftware.smackx.pubsub.packet.PubSub;
+import org.jivesoftware.smackx.pubsub.packet.SyncPacketSend;
+
+/**
+ * The main class for the majority of pubsub functionality. In general
+ * almost all pubsub capabilities are related to the concept of a node.
+ * All items are published to a node, and typically subscribed to by other
+ * users. These users then retrieve events based on this subscription.
+ *
+ * @author Robin Collier
+ */
+public class LeafNode extends Node
+{
+ LeafNode(Connection connection, String nodeName)
+ {
+ super(connection, nodeName);
+ }
+
+ /**
+ * Get information on the items in the node in standard
+ * {@link DiscoverItems} format.
+ *
+ * @return The item details in {@link DiscoverItems} format
+ *
+ * @throws XMPPException
+ */
+ public DiscoverItems discoverItems()
+ throws XMPPException
+ {
+ DiscoverItems items = new DiscoverItems();
+ items.setTo(to);
+ items.setNode(getId());
+ return (DiscoverItems)SyncPacketSend.getReply(con, items);
+ }
+
+ /**
+ * Get the current items stored in the node.
+ *
+ * @return List of {@link Item} in the node
+ *
+ * @throws XMPPException
+ */
+ public <T extends Item> List<T> getItems()
+ throws XMPPException
+ {
+ PubSub request = createPubsubPacket(Type.GET, new GetItemsRequest(getId()));
+
+ PubSub result = (PubSub)SyncPacketSend.getReply(con, request);
+ ItemsExtension itemsElem = (ItemsExtension)result.getExtension(PubSubElementType.ITEMS);
+ return (List<T>)itemsElem.getItems();
+ }
+
+ /**
+ * Get the current items stored in the node based
+ * on the subscription associated with the provided
+ * subscription id.
+ *
+ * @param subscriptionId - The subscription id for the
+ * associated subscription.
+ * @return List of {@link Item} in the node
+ *
+ * @throws XMPPException
+ */
+ public <T extends Item> List<T> getItems(String subscriptionId)
+ throws XMPPException
+ {
+ PubSub request = createPubsubPacket(Type.GET, new GetItemsRequest(getId(), subscriptionId));
+
+ PubSub result = (PubSub)SyncPacketSend.getReply(con, request);
+ ItemsExtension itemsElem = (ItemsExtension)result.getExtension(PubSubElementType.ITEMS);
+ return (List<T>)itemsElem.getItems();
+ }
+
+ /**
+ * Get the items specified from the node. This would typically be
+ * used when the server does not return the payload due to size
+ * constraints. The user would be required to retrieve the payload
+ * after the items have been retrieved via {@link #getItems()} or an
+ * event, that did not include the payload.
+ *
+ * @param ids Item ids of the items to retrieve
+ *
+ * @return The list of {@link Item} with payload
+ *
+ * @throws XMPPException
+ */
+ public <T extends Item> List<T> getItems(Collection<String> ids)
+ throws XMPPException
+ {
+ List<Item> itemList = new ArrayList<Item>(ids.size());
+
+ for (String id : ids)
+ {
+ itemList.add(new Item(id));
+ }
+ PubSub request = createPubsubPacket(Type.GET, new ItemsExtension(ItemsExtension.ItemsElementType.items, getId(), itemList));
+
+ PubSub result = (PubSub)SyncPacketSend.getReply(con, request);
+ ItemsExtension itemsElem = (ItemsExtension)result.getExtension(PubSubElementType.ITEMS);
+ return (List<T>)itemsElem.getItems();
+ }
+
+ /**
+ * Get items persisted on the node, limited to the specified number.
+ *
+ * @param maxItems Maximum number of items to return
+ *
+ * @return List of {@link Item}
+ *
+ * @throws XMPPException
+ */
+ public <T extends Item> List<T> getItems(int maxItems)
+ throws XMPPException
+ {
+ PubSub request = createPubsubPacket(Type.GET, new GetItemsRequest(getId(), maxItems));
+
+ PubSub result = (PubSub)SyncPacketSend.getReply(con, request);
+ ItemsExtension itemsElem = (ItemsExtension)result.getExtension(PubSubElementType.ITEMS);
+ return (List<T>)itemsElem.getItems();
+ }
+
+ /**
+ * Get items persisted on the node, limited to the specified number
+ * based on the subscription associated with the provided subscriptionId.
+ *
+ * @param maxItems Maximum number of items to return
+ * @param subscriptionId The subscription which the retrieval is based
+ * on.
+ *
+ * @return List of {@link Item}
+ *
+ * @throws XMPPException
+ */
+ public <T extends Item> List<T> getItems(int maxItems, String subscriptionId)
+ throws XMPPException
+ {
+ PubSub request = createPubsubPacket(Type.GET, new GetItemsRequest(getId(), subscriptionId, maxItems));
+
+ PubSub result = (PubSub)SyncPacketSend.getReply(con, request);
+ ItemsExtension itemsElem = (ItemsExtension)result.getExtension(PubSubElementType.ITEMS);
+ return (List<T>)itemsElem.getItems();
+ }
+
+ /**
+ * Publishes an event to the node. This is an empty event
+ * with no item.
+ *
+ * This is only acceptable for nodes with {@link ConfigureForm#isPersistItems()}=false
+ * and {@link ConfigureForm#isDeliverPayloads()}=false.
+ *
+ * This is an asynchronous call which returns as soon as the
+ * packet has been sent.
+ *
+ * For synchronous calls use {@link #send() send()}.
+ */
+ public void publish()
+ {
+ PubSub packet = createPubsubPacket(Type.SET, new NodeExtension(PubSubElementType.PUBLISH, getId()));
+
+ con.sendPacket(packet);
+ }
+
+ /**
+ * Publishes an event to the node. This is a simple item
+ * with no payload.
+ *
+ * If the id is null, an empty item (one without an id) will be sent.
+ * Please note that this is not the same as {@link #send()}, which
+ * publishes an event with NO item.
+ *
+ * This is an asynchronous call which returns as soon as the
+ * packet has been sent.
+ *
+ * For synchronous calls use {@link #send(Item) send(Item))}.
+ *
+ * @param item - The item being sent
+ */
+ public <T extends Item> void publish(T item)
+ {
+ Collection<T> items = new ArrayList<T>(1);
+ items.add((T)(item == null ? new Item() : item));
+ publish(items);
+ }
+
+ /**
+ * Publishes multiple events to the node. Same rules apply as in {@link #publish(Item)}.
+ *
+ * In addition, if {@link ConfigureForm#isPersistItems()}=false, only the last item in the input
+ * list will get stored on the node, assuming it stores the last sent item.
+ *
+ * This is an asynchronous call which returns as soon as the
+ * packet has been sent.
+ *
+ * For synchronous calls use {@link #send(Collection) send(Collection))}.
+ *
+ * @param items - The collection of items being sent
+ */
+ public <T extends Item> void publish(Collection<T> items)
+ {
+ PubSub packet = createPubsubPacket(Type.SET, new PublishItem<T>(getId(), items));
+
+ con.sendPacket(packet);
+ }
+
+ /**
+ * Publishes an event to the node. This is an empty event
+ * with no item.
+ *
+ * This is only acceptable for nodes with {@link ConfigureForm#isPersistItems()}=false
+ * and {@link ConfigureForm#isDeliverPayloads()}=false.
+ *
+ * This is a synchronous call which will throw an exception
+ * on failure.
+ *
+ * For asynchronous calls, use {@link #publish() publish()}.
+ *
+ * @throws XMPPException
+ */
+ public void send()
+ throws XMPPException
+ {
+ PubSub packet = createPubsubPacket(Type.SET, new NodeExtension(PubSubElementType.PUBLISH, getId()));
+
+ SyncPacketSend.getReply(con, packet);
+ }
+
+ /**
+ * Publishes an event to the node. This can be either a simple item
+ * with no payload, or one with it. This is determined by the Node
+ * configuration.
+ *
+ * If the node has <b>deliver_payload=false</b>, the Item must not
+ * have a payload.
+ *
+ * If the id is null, an empty item (one without an id) will be sent.
+ * Please note that this is not the same as {@link #send()}, which
+ * publishes an event with NO item.
+ *
+ * This is a synchronous call which will throw an exception
+ * on failure.
+ *
+ * For asynchronous calls, use {@link #publish(Item) publish(Item)}.
+ *
+ * @param item - The item being sent
+ *
+ * @throws XMPPException
+ */
+ public <T extends Item> void send(T item)
+ throws XMPPException
+ {
+ Collection<T> items = new ArrayList<T>(1);
+ items.add((item == null ? (T)new Item() : item));
+ send(items);
+ }
+
+ /**
+ * Publishes multiple events to the node. Same rules apply as in {@link #send(Item)}.
+ *
+ * In addition, if {@link ConfigureForm#isPersistItems()}=false, only the last item in the input
+ * list will get stored on the node, assuming it stores the last sent item.
+ *
+ * This is a synchronous call which will throw an exception
+ * on failure.
+ *
+ * For asynchronous calls, use {@link #publish(Collection) publish(Collection))}.
+ *
+ * @param items - The collection of {@link Item} objects being sent
+ *
+ * @throws XMPPException
+ */
+ public <T extends Item> void send(Collection<T> items)
+ throws XMPPException
+ {
+ PubSub packet = createPubsubPacket(Type.SET, new PublishItem<T>(getId(), items));
+
+ SyncPacketSend.getReply(con, packet);
+ }
+
+ /**
+ * Purges the node of all items.
+ *
+ * <p>Note: Some implementations may keep the last item
+ * sent.
+ *
+ * @throws XMPPException
+ */
+ public void deleteAllItems()
+ throws XMPPException
+ {
+ PubSub request = createPubsubPacket(Type.SET, new NodeExtension(PubSubElementType.PURGE_OWNER, getId()), PubSubElementType.PURGE_OWNER.getNamespace());
+
+ SyncPacketSend.getReply(con, request);
+ }
+
+ /**
+ * Delete the item with the specified id from the node.
+ *
+ * @param itemId The id of the item
+ *
+ * @throws XMPPException
+ */
+ public void deleteItem(String itemId)
+ throws XMPPException
+ {
+ Collection<String> items = new ArrayList<String>(1);
+ items.add(itemId);
+ deleteItem(items);
+ }
+
+ /**
+ * Delete the items with the specified id's from the node.
+ *
+ * @param itemIds The list of id's of items to delete
+ *
+ * @throws XMPPException
+ */
+ public void deleteItem(Collection<String> itemIds)
+ throws XMPPException
+ {
+ List<Item> items = new ArrayList<Item>(itemIds.size());
+
+ for (String id : itemIds)
+ {
+ items.add(new Item(id));
+ }
+ PubSub request = createPubsubPacket(Type.SET, new ItemsExtension(ItemsExtension.ItemsElementType.retract, getId(), items));
+ SyncPacketSend.getReply(con, request);
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/Node.java b/src/org/jivesoftware/smackx/pubsub/Node.java new file mode 100644 index 0000000..1b0ff5a --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/Node.java @@ -0,0 +1,541 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2009 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.smackx.pubsub;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.OrFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.packet.IQ.Type;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.packet.DelayInformation;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.Header;
+import org.jivesoftware.smackx.packet.HeadersExtension;
+import org.jivesoftware.smackx.pubsub.listener.ItemDeleteListener;
+import org.jivesoftware.smackx.pubsub.listener.ItemEventListener;
+import org.jivesoftware.smackx.pubsub.listener.NodeConfigListener;
+import org.jivesoftware.smackx.pubsub.packet.PubSub;
+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
+import org.jivesoftware.smackx.pubsub.packet.SyncPacketSend;
+import org.jivesoftware.smackx.pubsub.util.NodeUtils;
+
+abstract public class Node
+{
+ protected Connection con;
+ protected String id;
+ protected String to;
+
+ protected ConcurrentHashMap<ItemEventListener<Item>, PacketListener> itemEventToListenerMap = new ConcurrentHashMap<ItemEventListener<Item>, PacketListener>();
+ protected ConcurrentHashMap<ItemDeleteListener, PacketListener> itemDeleteToListenerMap = new ConcurrentHashMap<ItemDeleteListener, PacketListener>();
+ protected ConcurrentHashMap<NodeConfigListener, PacketListener> configEventToListenerMap = new ConcurrentHashMap<NodeConfigListener, PacketListener>();
+
+ /**
+ * Construct a node associated to the supplied connection with the specified
+ * node id.
+ *
+ * @param connection The connection the node is associated with
+ * @param nodeName The node id
+ */
+ Node(Connection connection, String nodeName)
+ {
+ con = connection;
+ id = nodeName;
+ }
+
+ /**
+ * Some XMPP servers may require a specific service to be addressed on the
+ * server.
+ *
+ * For example, OpenFire requires the server to be prefixed by <b>pubsub</b>
+ */
+ void setTo(String toAddress)
+ {
+ to = toAddress;
+ }
+
+ /**
+ * Get the NodeId
+ *
+ * @return the node id
+ */
+ public String getId()
+ {
+ return id;
+ }
+ /**
+ * Returns a configuration form, from which you can create an answer form to be submitted
+ * via the {@link #sendConfigurationForm(Form)}.
+ *
+ * @return the configuration form
+ */
+ public ConfigureForm getNodeConfiguration()
+ throws XMPPException
+ {
+ Packet reply = sendPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.CONFIGURE_OWNER, getId()), PubSubNamespace.OWNER);
+ return NodeUtils.getFormFromPacket(reply, PubSubElementType.CONFIGURE_OWNER);
+ }
+
+ /**
+ * Update the configuration with the contents of the new {@link Form}
+ *
+ * @param submitForm
+ */
+ public void sendConfigurationForm(Form submitForm)
+ throws XMPPException
+ {
+ PubSub packet = createPubsubPacket(Type.SET, new FormNode(FormNodeType.CONFIGURE_OWNER, getId(), submitForm), PubSubNamespace.OWNER);
+ SyncPacketSend.getReply(con, packet);
+ }
+
+ /**
+ * Discover node information in standard {@link DiscoverInfo} format.
+ *
+ * @return The discovery information about the node.
+ *
+ * @throws XMPPException
+ */
+ public DiscoverInfo discoverInfo()
+ throws XMPPException
+ {
+ DiscoverInfo info = new DiscoverInfo();
+ info.setTo(to);
+ info.setNode(getId());
+ return (DiscoverInfo)SyncPacketSend.getReply(con, info);
+ }
+
+ /**
+ * Get the subscriptions currently associated with this node.
+ *
+ * @return List of {@link Subscription}
+ *
+ * @throws XMPPException
+ */
+ public List<Subscription> getSubscriptions()
+ throws XMPPException
+ {
+ PubSub reply = (PubSub)sendPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.SUBSCRIPTIONS, getId()));
+ SubscriptionsExtension subElem = (SubscriptionsExtension)reply.getExtension(PubSubElementType.SUBSCRIPTIONS);
+ return subElem.getSubscriptions();
+ }
+
+ /**
+ * The user subscribes to the node using the supplied jid. The
+ * bare jid portion of this one must match the jid for the connection.
+ *
+ * Please note that the {@link Subscription.State} should be checked
+ * on return since more actions may be required by the caller.
+ * {@link Subscription.State#pending} - The owner must approve the subscription
+ * request before messages will be received.
+ * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true,
+ * the caller must configure the subscription before messages will be received. If it is false
+ * the caller can configure it but is not required to do so.
+ * @param jid The jid to subscribe as.
+ * @return The subscription
+ * @exception XMPPException
+ */
+ public Subscription subscribe(String jid)
+ throws XMPPException
+ {
+ PubSub reply = (PubSub)sendPubsubPacket(Type.SET, new SubscribeExtension(jid, getId()));
+ return (Subscription)reply.getExtension(PubSubElementType.SUBSCRIPTION);
+ }
+
+ /**
+ * The user subscribes to the node using the supplied jid and subscription
+ * options. The bare jid portion of this one must match the jid for the
+ * connection.
+ *
+ * Please note that the {@link Subscription.State} should be checked
+ * on return since more actions may be required by the caller.
+ * {@link Subscription.State#pending} - The owner must approve the subscription
+ * request before messages will be received.
+ * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true,
+ * the caller must configure the subscription before messages will be received. If it is false
+ * the caller can configure it but is not required to do so.
+ * @param jid The jid to subscribe as.
+ * @return The subscription
+ * @exception XMPPException
+ */
+ public Subscription subscribe(String jid, SubscribeForm subForm)
+ throws XMPPException
+ {
+ PubSub request = createPubsubPacket(Type.SET, new SubscribeExtension(jid, getId()));
+ request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm));
+ PubSub reply = (PubSub)PubSubManager.sendPubsubPacket(con, jid, Type.SET, request);
+ return (Subscription)reply.getExtension(PubSubElementType.SUBSCRIPTION);
+ }
+
+ /**
+ * Remove the subscription related to the specified JID. This will only
+ * work if there is only 1 subscription. If there are multiple subscriptions,
+ * use {@link #unsubscribe(String, String)}.
+ *
+ * @param jid The JID used to subscribe to the node
+ *
+ * @throws XMPPException
+ */
+ public void unsubscribe(String jid)
+ throws XMPPException
+ {
+ unsubscribe(jid, null);
+ }
+
+ /**
+ * Remove the specific subscription related to the specified JID.
+ *
+ * @param jid The JID used to subscribe to the node
+ * @param subscriptionId The id of the subscription being removed
+ *
+ * @throws XMPPException
+ */
+ public void unsubscribe(String jid, String subscriptionId)
+ throws XMPPException
+ {
+ sendPubsubPacket(Type.SET, new UnsubscribeExtension(jid, getId(), subscriptionId));
+ }
+
+ /**
+ * Returns a SubscribeForm for subscriptions, from which you can create an answer form to be submitted
+ * via the {@link #sendConfigurationForm(Form)}.
+ *
+ * @return A subscription options form
+ *
+ * @throws XMPPException
+ */
+ public SubscribeForm getSubscriptionOptions(String jid)
+ throws XMPPException
+ {
+ return getSubscriptionOptions(jid, null);
+ }
+
+
+ /**
+ * Get the options for configuring the specified subscription.
+ *
+ * @param jid JID the subscription is registered under
+ * @param subscriptionId The subscription id
+ *
+ * @return The subscription option form
+ *
+ * @throws XMPPException
+ */
+ public SubscribeForm getSubscriptionOptions(String jid, String subscriptionId)
+ throws XMPPException
+ {
+ PubSub packet = (PubSub)sendPubsubPacket(Type.GET, new OptionsExtension(jid, getId(), subscriptionId));
+ FormNode ext = (FormNode)packet.getExtension(PubSubElementType.OPTIONS);
+ return new SubscribeForm(ext.getForm());
+ }
+
+ /**
+ * Register a listener for item publication events. This
+ * listener will get called whenever an item is published to
+ * this node.
+ *
+ * @param listener The handler for the event
+ */
+ public void addItemEventListener(ItemEventListener listener)
+ {
+ PacketListener conListener = new ItemEventTranslator(listener);
+ itemEventToListenerMap.put(listener, conListener);
+ con.addPacketListener(conListener, new EventContentFilter(EventElementType.items.toString(), "item"));
+ }
+
+ /**
+ * Unregister a listener for publication events.
+ *
+ * @param listener The handler to unregister
+ */
+ public void removeItemEventListener(ItemEventListener listener)
+ {
+ PacketListener conListener = itemEventToListenerMap.remove(listener);
+
+ if (conListener != null)
+ con.removePacketListener(conListener);
+ }
+
+ /**
+ * Register a listener for configuration events. This listener
+ * will get called whenever the node's configuration changes.
+ *
+ * @param listener The handler for the event
+ */
+ public void addConfigurationListener(NodeConfigListener listener)
+ {
+ PacketListener conListener = new NodeConfigTranslator(listener);
+ configEventToListenerMap.put(listener, conListener);
+ con.addPacketListener(conListener, new EventContentFilter(EventElementType.configuration.toString()));
+ }
+
+ /**
+ * Unregister a listener for configuration events.
+ *
+ * @param listener The handler to unregister
+ */
+ public void removeConfigurationListener(NodeConfigListener listener)
+ {
+ PacketListener conListener = configEventToListenerMap .remove(listener);
+
+ if (conListener != null)
+ con.removePacketListener(conListener);
+ }
+
+ /**
+ * Register an listener for item delete events. This listener
+ * gets called whenever an item is deleted from the node.
+ *
+ * @param listener The handler for the event
+ */
+ public void addItemDeleteListener(ItemDeleteListener listener)
+ {
+ PacketListener delListener = new ItemDeleteTranslator(listener);
+ itemDeleteToListenerMap.put(listener, delListener);
+ EventContentFilter deleteItem = new EventContentFilter(EventElementType.items.toString(), "retract");
+ EventContentFilter purge = new EventContentFilter(EventElementType.purge.toString());
+
+ con.addPacketListener(delListener, new OrFilter(deleteItem, purge));
+ }
+
+ /**
+ * Unregister a listener for item delete events.
+ *
+ * @param listener The handler to unregister
+ */
+ public void removeItemDeleteListener(ItemDeleteListener listener)
+ {
+ PacketListener conListener = itemDeleteToListenerMap .remove(listener);
+
+ if (conListener != null)
+ con.removePacketListener(conListener);
+ }
+
+ @Override
+ public String toString()
+ {
+ return super.toString() + " " + getClass().getName() + " id: " + id;
+ }
+
+ protected PubSub createPubsubPacket(Type type, PacketExtension ext)
+ {
+ return createPubsubPacket(type, ext, null);
+ }
+
+ protected PubSub createPubsubPacket(Type type, PacketExtension ext, PubSubNamespace ns)
+ {
+ return PubSubManager.createPubsubPacket(to, type, ext, ns);
+ }
+
+ protected Packet sendPubsubPacket(Type type, NodeExtension ext)
+ throws XMPPException
+ {
+ return PubSubManager.sendPubsubPacket(con, to, type, ext);
+ }
+
+ protected Packet sendPubsubPacket(Type type, NodeExtension ext, PubSubNamespace ns)
+ throws XMPPException
+ {
+ return PubSubManager.sendPubsubPacket(con, to, type, ext, ns);
+ }
+
+
+ private static List<String> getSubscriptionIds(Packet packet)
+ {
+ HeadersExtension headers = (HeadersExtension)packet.getExtension("headers", "http://jabber.org/protocol/shim");
+ List<String> values = null;
+
+ if (headers != null)
+ {
+ values = new ArrayList<String>(headers.getHeaders().size());
+
+ for (Header header : headers.getHeaders())
+ {
+ values.add(header.getValue());
+ }
+ }
+ return values;
+ }
+
+ /**
+ * This class translates low level item publication events into api level objects for
+ * user consumption.
+ *
+ * @author Robin Collier
+ */
+ public class ItemEventTranslator implements PacketListener
+ {
+ private ItemEventListener listener;
+
+ public ItemEventTranslator(ItemEventListener eventListener)
+ {
+ listener = eventListener;
+ }
+
+ public void processPacket(Packet packet)
+ {
+ EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
+ ItemsExtension itemsElem = (ItemsExtension)event.getEvent();
+ DelayInformation delay = (DelayInformation)packet.getExtension("delay", "urn:xmpp:delay");
+
+ // If there was no delay based on XEP-0203, then try XEP-0091 for backward compatibility
+ if (delay == null)
+ {
+ delay = (DelayInformation)packet.getExtension("x", "jabber:x:delay");
+ }
+ ItemPublishEvent eventItems = new ItemPublishEvent(itemsElem.getNode(), (List<Item>)itemsElem.getItems(), getSubscriptionIds(packet), (delay == null ? null : delay.getStamp()));
+ listener.handlePublishedItems(eventItems);
+ }
+ }
+
+ /**
+ * This class translates low level item deletion events into api level objects for
+ * user consumption.
+ *
+ * @author Robin Collier
+ */
+ public class ItemDeleteTranslator implements PacketListener
+ {
+ private ItemDeleteListener listener;
+
+ public ItemDeleteTranslator(ItemDeleteListener eventListener)
+ {
+ listener = eventListener;
+ }
+
+ public void processPacket(Packet packet)
+ {
+ EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
+
+ List<PacketExtension> extList = event.getExtensions();
+
+ if (extList.get(0).getElementName().equals(PubSubElementType.PURGE_EVENT.getElementName()))
+ {
+ listener.handlePurge();
+ }
+ else
+ {
+ ItemsExtension itemsElem = (ItemsExtension)event.getEvent();
+ Collection<? extends PacketExtension> pubItems = itemsElem.getItems();
+ Iterator<RetractItem> it = (Iterator<RetractItem>)pubItems.iterator();
+ List<String> items = new ArrayList<String>(pubItems.size());
+
+ while (it.hasNext())
+ {
+ RetractItem item = it.next();
+ items.add(item.getId());
+ }
+
+ ItemDeleteEvent eventItems = new ItemDeleteEvent(itemsElem.getNode(), items, getSubscriptionIds(packet));
+ listener.handleDeletedItems(eventItems);
+ }
+ }
+ }
+
+ /**
+ * This class translates low level node configuration events into api level objects for
+ * user consumption.
+ *
+ * @author Robin Collier
+ */
+ public class NodeConfigTranslator implements PacketListener
+ {
+ private NodeConfigListener listener;
+
+ public NodeConfigTranslator(NodeConfigListener eventListener)
+ {
+ listener = eventListener;
+ }
+
+ public void processPacket(Packet packet)
+ {
+ EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
+ ConfigurationEvent config = (ConfigurationEvent)event.getEvent();
+
+ listener.handleNodeConfiguration(config);
+ }
+ }
+
+ /**
+ * Filter for {@link PacketListener} to filter out events not specific to the
+ * event type expected for this node.
+ *
+ * @author Robin Collier
+ */
+ class EventContentFilter implements PacketFilter
+ {
+ private String firstElement;
+ private String secondElement;
+
+ EventContentFilter(String elementName)
+ {
+ firstElement = elementName;
+ }
+
+ EventContentFilter(String firstLevelEelement, String secondLevelElement)
+ {
+ firstElement = firstLevelEelement;
+ secondElement = secondLevelElement;
+ }
+
+ public boolean accept(Packet packet)
+ {
+ if (!(packet instanceof Message))
+ return false;
+
+ EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());
+
+ if (event == null)
+ return false;
+
+ NodeExtension embedEvent = event.getEvent();
+
+ if (embedEvent == null)
+ return false;
+
+ if (embedEvent.getElementName().equals(firstElement))
+ {
+ if (!embedEvent.getNode().equals(getId()))
+ return false;
+
+ if (secondElement == null)
+ return true;
+
+ if (embedEvent instanceof EmbeddedPacketExtension)
+ {
+ List<PacketExtension> secondLevelList = ((EmbeddedPacketExtension)embedEvent).getExtensions();
+
+ if (secondLevelList.size() > 0 && secondLevelList.get(0).getElementName().equals(secondElement))
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/NodeEvent.java b/src/org/jivesoftware/smackx/pubsub/NodeEvent.java new file mode 100644 index 0000000..1392e85 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/NodeEvent.java @@ -0,0 +1,35 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2009 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.smackx.pubsub;
+
+abstract public class NodeEvent
+{
+ private String nodeId;
+
+ protected NodeEvent(String id)
+ {
+ nodeId = id;
+ }
+
+ public String getNodeId()
+ {
+ return nodeId;
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/NodeExtension.java b/src/org/jivesoftware/smackx/pubsub/NodeExtension.java new file mode 100644 index 0000000..7e4cdec --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/NodeExtension.java @@ -0,0 +1,85 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * A class which represents a common element within the pubsub defined
+ * schemas. One which has a <b>node</b> as an attribute. This class is
+ * used on its own as well as a base class for many others, since the
+ * node is a central concept to most pubsub functionality.
+ *
+ * @author Robin Collier
+ */
+public class NodeExtension implements PacketExtension
+{
+ private PubSubElementType element;
+ private String node;
+
+ /**
+ * Constructs a <tt>NodeExtension</tt> with an element name specified
+ * by {@link PubSubElementType} and the specified node id.
+ *
+ * @param elem Defines the element name and namespace
+ * @param nodeId Specifies the id of the node
+ */
+ public NodeExtension(PubSubElementType elem, String nodeId)
+ {
+ element = elem;
+ this.node = nodeId;
+ }
+
+ /**
+ * Constructs a <tt>NodeExtension</tt> with an element name specified
+ * by {@link PubSubElementType}.
+ *
+ * @param elem Defines the element name and namespace
+ */
+ public NodeExtension(PubSubElementType elem)
+ {
+ this(elem, null);
+ }
+
+ /**
+ * Gets the node id
+ *
+ * @return The node id
+ */
+ public String getNode()
+ {
+ return node;
+ }
+
+ public String getElementName()
+ {
+ return element.getElementName();
+ }
+
+ public String getNamespace()
+ {
+ return element.getNamespace().getXmlns();
+ }
+
+ public String toXML()
+ {
+ return '<' + getElementName() + (node == null ? "" : " node='" + node + '\'') + "/>";
+ }
+
+ @Override
+ public String toString()
+ {
+ return getClass().getName() + " - content [" + toXML() + "]";
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/NodeType.java b/src/org/jivesoftware/smackx/pubsub/NodeType.java new file mode 100644 index 0000000..5ee5a05 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/NodeType.java @@ -0,0 +1,25 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+/**
+ * Defines the available types of nodes
+ *
+ * @author Robin Collier
+ */
+public enum NodeType
+{
+ leaf,
+ collection;
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/OptionsExtension.java b/src/org/jivesoftware/smackx/pubsub/OptionsExtension.java new file mode 100644 index 0000000..32c0331 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/OptionsExtension.java @@ -0,0 +1,72 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import org.jivesoftware.smackx.pubsub.util.XmlUtils;
+
+/**
+ * A packet extension representing the <b>options</b> element.
+ *
+ * @author Robin Collier
+ */
+public class OptionsExtension extends NodeExtension
+{
+ protected String jid;
+ protected String id;
+
+ public OptionsExtension(String subscriptionJid)
+ {
+ this(subscriptionJid, null, null);
+ }
+
+ public OptionsExtension(String subscriptionJid, String nodeId)
+ {
+ this(subscriptionJid, nodeId, null);
+ }
+
+ public OptionsExtension(String jid, String nodeId, String subscriptionId)
+ {
+ super(PubSubElementType.OPTIONS, nodeId);
+ this.jid = jid;
+ id = subscriptionId;
+ }
+
+ public String getJid()
+ {
+ return jid;
+ }
+
+ public String getId()
+ {
+ return id;
+ }
+
+ @Override
+ public String toXML()
+ {
+ StringBuilder builder = new StringBuilder("<");
+ builder.append(getElementName());
+ XmlUtils.appendAttribute(builder, "jid", jid);
+
+ if (getNode() != null)
+ XmlUtils.appendAttribute(builder, "node", getNode());
+
+ if (id != null)
+ XmlUtils.appendAttribute(builder, "subid", id);
+
+ builder.append("/>");
+ return builder.toString();
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/PayloadItem.java b/src/org/jivesoftware/smackx/pubsub/PayloadItem.java new file mode 100644 index 0000000..488fd97 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/PayloadItem.java @@ -0,0 +1,138 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.pubsub.provider.ItemProvider;
+
+/**
+ * This class represents an item that has been, or will be published to a
+ * pubsub node. An <tt>Item</tt> has several properties that are dependent
+ * on the configuration of the node to which it has been or will be published.
+ *
+ * <h1>An Item received from a node (via {@link LeafNode#getItems()} or {@link LeafNode#addItemEventListener(org.jivesoftware.smackx.pubsub.listener.ItemEventListener)}</b>
+ * <li>Will always have an id (either user or server generated) unless node configuration has both
+ * {@link ConfigureForm#isPersistItems()} and {@link ConfigureForm#isDeliverPayloads()}set to false.
+ * <li>Will have a payload if the node configuration has {@link ConfigureForm#isDeliverPayloads()} set
+ * to true, otherwise it will be null.
+ *
+ * <h1>An Item created to send to a node (via {@link LeafNode#send()} or {@link LeafNode#publish()}</b>
+ * <li>The id is optional, since the server will generate one if necessary, but should be used if it is
+ * meaningful in the context of the node. This value must be unique within the node that it is sent to, since
+ * resending an item with the same id will overwrite the one that already exists if the items are persisted.
+ * <li>Will require payload if the node configuration has {@link ConfigureForm#isDeliverPayloads()} set
+ * to true.
+ *
+ * <p>To customise the payload object being returned from the {@link #getPayload()} method, you can
+ * add a custom parser as explained in {@link ItemProvider}.
+ *
+ * @author Robin Collier
+ */
+public class PayloadItem<E extends PacketExtension> extends Item
+{
+ private E payload;
+
+ /**
+ * Create an <tt>Item</tt> with no id and a payload The id will be set by the server.
+ *
+ * @param payloadExt A {@link PacketExtension} which represents the payload data.
+ */
+ public PayloadItem(E payloadExt)
+ {
+ super();
+
+ if (payloadExt == null)
+ throw new IllegalArgumentException("payload cannot be 'null'");
+ payload = payloadExt;
+ }
+
+ /**
+ * Create an <tt>Item</tt> with an id and payload.
+ *
+ * @param itemId The id of this item. It can be null if we want the server to set the id.
+ * @param payloadExt A {@link PacketExtension} which represents the payload data.
+ */
+ public PayloadItem(String itemId, E payloadExt)
+ {
+ super(itemId);
+
+ if (payloadExt == null)
+ throw new IllegalArgumentException("payload cannot be 'null'");
+ payload = payloadExt;
+ }
+
+ /**
+ * Create an <tt>Item</tt> with an id, node id and payload.
+ *
+ * <p>
+ * <b>Note:</b> This is not valid for publishing an item to a node, only receiving from
+ * one as part of {@link Message}. If used to create an Item to publish
+ * (via {@link LeafNode#publish(Item)}, the server <i>may</i> return an
+ * error for an invalid packet.
+ *
+ * @param itemId The id of this item.
+ * @param nodeId The id of the node the item was published to.
+ * @param payloadExt A {@link PacketExtension} which represents the payload data.
+ */
+ public PayloadItem(String itemId, String nodeId, E payloadExt)
+ {
+ super(itemId, nodeId);
+
+ if (payloadExt == null)
+ throw new IllegalArgumentException("payload cannot be 'null'");
+ payload = payloadExt;
+ }
+
+ /**
+ * Get the payload associated with this <tt>Item</tt>. Customising the payload
+ * parsing from the server can be accomplished as described in {@link ItemProvider}.
+ *
+ * @return The payload as a {@link PacketExtension}.
+ */
+ public E getPayload()
+ {
+ return payload;
+ }
+
+ @Override
+ public String toXML()
+ {
+ StringBuilder builder = new StringBuilder("<item");
+
+ if (getId() != null)
+ {
+ builder.append(" id='");
+ builder.append(getId());
+ builder.append("'");
+ }
+
+ if (getNode() != null) {
+ builder.append(" node='");
+ builder.append(getNode());
+ builder.append("'");
+ }
+ builder.append(">");
+ builder.append(payload.toXML());
+ builder.append("</item>");
+
+ return builder.toString();
+ }
+
+ @Override
+ public String toString()
+ {
+ return getClass().getName() + " | Content [" + toXML() + "]";
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/PresenceState.java b/src/org/jivesoftware/smackx/pubsub/PresenceState.java new file mode 100644 index 0000000..0612fc2 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/PresenceState.java @@ -0,0 +1,25 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+/**
+ * Defines the possible valid presence states for node subscription via
+ * {@link SubscribeForm#getShowValues()}.
+ *
+ * @author Robin Collier
+ */
+public enum PresenceState
+{
+ chat, online, away, xa, dnd
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/PubSubElementType.java b/src/org/jivesoftware/smackx/pubsub/PubSubElementType.java new file mode 100644 index 0000000..a887ca2 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/PubSubElementType.java @@ -0,0 +1,80 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
+
+/**
+ * Defines all the possible element types as defined for all the pubsub
+ * schemas in all 3 namespaces.
+ *
+ * @author Robin Collier
+ */
+public enum PubSubElementType
+{
+ CREATE("create", PubSubNamespace.BASIC),
+ DELETE("delete", PubSubNamespace.OWNER),
+ DELETE_EVENT("delete", PubSubNamespace.EVENT),
+ CONFIGURE("configure", PubSubNamespace.BASIC),
+ CONFIGURE_OWNER("configure", PubSubNamespace.OWNER),
+ CONFIGURATION("configuration", PubSubNamespace.EVENT),
+ OPTIONS("options", PubSubNamespace.BASIC),
+ DEFAULT("default", PubSubNamespace.OWNER),
+ ITEMS("items", PubSubNamespace.BASIC),
+ ITEMS_EVENT("items", PubSubNamespace.EVENT),
+ ITEM("item", PubSubNamespace.BASIC),
+ ITEM_EVENT("item", PubSubNamespace.EVENT),
+ PUBLISH("publish", PubSubNamespace.BASIC),
+ PUBLISH_OPTIONS("publish-options", PubSubNamespace.BASIC),
+ PURGE_OWNER("purge", PubSubNamespace.OWNER),
+ PURGE_EVENT("purge", PubSubNamespace.EVENT),
+ RETRACT("retract", PubSubNamespace.BASIC),
+ AFFILIATIONS("affiliations", PubSubNamespace.BASIC),
+ SUBSCRIBE("subscribe", PubSubNamespace.BASIC),
+ SUBSCRIPTION("subscription", PubSubNamespace.BASIC),
+ SUBSCRIPTIONS("subscriptions", PubSubNamespace.BASIC),
+ UNSUBSCRIBE("unsubscribe", PubSubNamespace.BASIC);
+
+ private String eName;
+ private PubSubNamespace nSpace;
+
+ private PubSubElementType(String elemName, PubSubNamespace ns)
+ {
+ eName = elemName;
+ nSpace = ns;
+ }
+
+ public PubSubNamespace getNamespace()
+ {
+ return nSpace;
+ }
+
+ public String getElementName()
+ {
+ return eName;
+ }
+
+ public static PubSubElementType valueOfFromElemName(String elemName, String namespace)
+ {
+ int index = namespace.lastIndexOf('#');
+ String fragment = (index == -1 ? null : namespace.substring(index+1));
+
+ if (fragment != null)
+ {
+ return valueOf((elemName + '_' + fragment).toUpperCase());
+ }
+ return valueOf(elemName.toUpperCase().replace('-', '_'));
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/PubSubManager.java b/src/org/jivesoftware/smackx/pubsub/PubSubManager.java new file mode 100644 index 0000000..4fb0158 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/PubSubManager.java @@ -0,0 +1,329 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ.Type;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+import org.jivesoftware.smackx.pubsub.packet.PubSub;
+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
+import org.jivesoftware.smackx.pubsub.packet.SyncPacketSend;
+import org.jivesoftware.smackx.pubsub.util.NodeUtils;
+
+/**
+ * This is the starting point for access to the pubsub service. It
+ * will provide access to general information about the service, as
+ * well as create or retrieve pubsub {@link LeafNode} instances. These
+ * instances provide the bulk of the functionality as defined in the
+ * pubsub specification <a href="http://xmpp.org/extensions/xep-0060.html">XEP-0060</a>.
+ *
+ * @author Robin Collier
+ */
+final public class PubSubManager
+{
+ private Connection con;
+ private String to;
+ private Map<String, Node> nodeMap = new ConcurrentHashMap<String, Node>();
+
+ /**
+ * Create a pubsub manager associated to the specified connection. Defaults the service
+ * name to <i>pubsub</i>
+ *
+ * @param connection The XMPP connection
+ */
+ public PubSubManager(Connection connection)
+ {
+ con = connection;
+ to = "pubsub." + connection.getServiceName();
+ }
+
+ /**
+ * Create a pubsub manager associated to the specified connection where
+ * the pubsub requests require a specific to address for packets.
+ *
+ * @param connection The XMPP connection
+ * @param toAddress The pubsub specific to address (required for some servers)
+ */
+ public PubSubManager(Connection connection, String toAddress)
+ {
+ con = connection;
+ to = toAddress;
+ }
+
+ /**
+ * Creates an instant node, if supported.
+ *
+ * @return The node that was created
+ * @exception XMPPException
+ */
+ public LeafNode createNode()
+ throws XMPPException
+ {
+ PubSub reply = (PubSub)sendPubsubPacket(Type.SET, new NodeExtension(PubSubElementType.CREATE));
+ NodeExtension elem = (NodeExtension)reply.getExtension("create", PubSubNamespace.BASIC.getXmlns());
+
+ LeafNode newNode = new LeafNode(con, elem.getNode());
+ newNode.setTo(to);
+ nodeMap.put(newNode.getId(), newNode);
+
+ return newNode;
+ }
+
+ /**
+ * Creates a node with default configuration.
+ *
+ * @param id The id of the node, which must be unique within the
+ * pubsub service
+ * @return The node that was created
+ * @exception XMPPException
+ */
+ public LeafNode createNode(String id)
+ throws XMPPException
+ {
+ return (LeafNode)createNode(id, null);
+ }
+
+ /**
+ * Creates a node with specified configuration.
+ *
+ * Note: This is the only way to create a collection node.
+ *
+ * @param name The name of the node, which must be unique within the
+ * pubsub service
+ * @param config The configuration for the node
+ * @return The node that was created
+ * @exception XMPPException
+ */
+ public Node createNode(String name, Form config)
+ throws XMPPException
+ {
+ PubSub request = createPubsubPacket(to, Type.SET, new NodeExtension(PubSubElementType.CREATE, name));
+ boolean isLeafNode = true;
+
+ if (config != null)
+ {
+ request.addExtension(new FormNode(FormNodeType.CONFIGURE, config));
+ FormField nodeTypeField = config.getField(ConfigureNodeFields.node_type.getFieldName());
+
+ if (nodeTypeField != null)
+ isLeafNode = nodeTypeField.getValues().next().equals(NodeType.leaf.toString());
+ }
+
+ // Errors will cause exceptions in getReply, so it only returns
+ // on success.
+ sendPubsubPacket(con, to, Type.SET, request);
+ Node newNode = isLeafNode ? new LeafNode(con, name) : new CollectionNode(con, name);
+ newNode.setTo(to);
+ nodeMap.put(newNode.getId(), newNode);
+
+ return newNode;
+ }
+
+ /**
+ * Retrieves the requested node, if it exists. It will throw an
+ * exception if it does not.
+ *
+ * @param id - The unique id of the node
+ * @return the node
+ * @throws XMPPException The node does not exist
+ */
+ public <T extends Node> T getNode(String id)
+ throws XMPPException
+ {
+ Node node = nodeMap.get(id);
+
+ if (node == null)
+ {
+ DiscoverInfo info = new DiscoverInfo();
+ info.setTo(to);
+ info.setNode(id);
+
+ DiscoverInfo infoReply = (DiscoverInfo)SyncPacketSend.getReply(con, info);
+
+ if (infoReply.getIdentities().next().getType().equals(NodeType.leaf.toString()))
+ node = new LeafNode(con, id);
+ else
+ node = new CollectionNode(con, id);
+ node.setTo(to);
+ nodeMap.put(id, node);
+ }
+ return (T) node;
+ }
+
+ /**
+ * Get all the nodes that currently exist as a child of the specified
+ * collection node. If the service does not support collection nodes
+ * then all nodes will be returned.
+ *
+ * To retrieve contents of the root collection node (if it exists),
+ * or there is no root collection node, pass null as the nodeId.
+ *
+ * @param nodeId - The id of the collection node for which the child
+ * nodes will be returned.
+ * @return {@link DiscoverItems} representing the existing nodes
+ *
+ * @throws XMPPException
+ */
+ public DiscoverItems discoverNodes(String nodeId)
+ throws XMPPException
+ {
+ DiscoverItems items = new DiscoverItems();
+
+ if (nodeId != null)
+ items.setNode(nodeId);
+ items.setTo(to);
+ DiscoverItems nodeItems = (DiscoverItems)SyncPacketSend.getReply(con, items);
+ return nodeItems;
+ }
+
+ /**
+ * Gets the subscriptions on the root node.
+ *
+ * @return List of exceptions
+ *
+ * @throws XMPPException
+ */
+ public List<Subscription> getSubscriptions()
+ throws XMPPException
+ {
+ Packet reply = sendPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.SUBSCRIPTIONS));
+ SubscriptionsExtension subElem = (SubscriptionsExtension)reply.getExtension(PubSubElementType.SUBSCRIPTIONS.getElementName(), PubSubElementType.SUBSCRIPTIONS.getNamespace().getXmlns());
+ return subElem.getSubscriptions();
+ }
+
+ /**
+ * Gets the affiliations on the root node.
+ *
+ * @return List of affiliations
+ *
+ * @throws XMPPException
+ */
+ public List<Affiliation> getAffiliations()
+ throws XMPPException
+ {
+ PubSub reply = (PubSub)sendPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.AFFILIATIONS));
+ AffiliationsExtension listElem = (AffiliationsExtension)reply.getExtension(PubSubElementType.AFFILIATIONS);
+ return listElem.getAffiliations();
+ }
+
+ /**
+ * Delete the specified node
+ *
+ * @param nodeId
+ * @throws XMPPException
+ */
+ public void deleteNode(String nodeId)
+ throws XMPPException
+ {
+ sendPubsubPacket(Type.SET, new NodeExtension(PubSubElementType.DELETE, nodeId), PubSubElementType.DELETE.getNamespace());
+ nodeMap.remove(nodeId);
+ }
+
+ /**
+ * Returns the default settings for Node configuration.
+ *
+ * @return configuration form containing the default settings.
+ */
+ public ConfigureForm getDefaultConfiguration()
+ throws XMPPException
+ {
+ // Errors will cause exceptions in getReply, so it only returns
+ // on success.
+ PubSub reply = (PubSub)sendPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.DEFAULT), PubSubElementType.DEFAULT.getNamespace());
+ return NodeUtils.getFormFromPacket(reply, PubSubElementType.DEFAULT);
+ }
+
+ /**
+ * Gets the supported features of the servers pubsub implementation
+ * as a standard {@link DiscoverInfo} instance.
+ *
+ * @return The supported features
+ *
+ * @throws XMPPException
+ */
+ public DiscoverInfo getSupportedFeatures()
+ throws XMPPException
+ {
+ ServiceDiscoveryManager mgr = ServiceDiscoveryManager.getInstanceFor(con);
+ return mgr.discoverInfo(to);
+ }
+
+ private Packet sendPubsubPacket(Type type, PacketExtension ext, PubSubNamespace ns)
+ throws XMPPException
+ {
+ return sendPubsubPacket(con, to, type, ext, ns);
+ }
+
+ private Packet sendPubsubPacket(Type type, PacketExtension ext)
+ throws XMPPException
+ {
+ return sendPubsubPacket(type, ext, null);
+ }
+
+ static PubSub createPubsubPacket(String to, Type type, PacketExtension ext)
+ {
+ return createPubsubPacket(to, type, ext, null);
+ }
+
+ static PubSub createPubsubPacket(String to, Type type, PacketExtension ext, PubSubNamespace ns)
+ {
+ PubSub request = new PubSub();
+ request.setTo(to);
+ request.setType(type);
+
+ if (ns != null)
+ {
+ request.setPubSubNamespace(ns);
+ }
+ request.addExtension(ext);
+
+ return request;
+ }
+
+ static Packet sendPubsubPacket(Connection con, String to, Type type, PacketExtension ext)
+ throws XMPPException
+ {
+ return sendPubsubPacket(con, to, type, ext, null);
+ }
+
+ static Packet sendPubsubPacket(Connection con, String to, Type type, PacketExtension ext, PubSubNamespace ns)
+ throws XMPPException
+ {
+ return SyncPacketSend.getReply(con, createPubsubPacket(to, type, ext, ns));
+ }
+
+ static Packet sendPubsubPacket(Connection con, String to, Type type, PubSub packet)
+ throws XMPPException
+ {
+ return sendPubsubPacket(con, to, type, packet, null);
+ }
+
+ static Packet sendPubsubPacket(Connection con, String to, Type type, PubSub packet, PubSubNamespace ns)
+ throws XMPPException
+ {
+ return SyncPacketSend.getReply(con, packet);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/PublishItem.java b/src/org/jivesoftware/smackx/pubsub/PublishItem.java new file mode 100644 index 0000000..ffbd705 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/PublishItem.java @@ -0,0 +1,70 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Represents a request to publish an item(s) to a specific node.
+ *
+ * @author Robin Collier
+ */
+public class PublishItem <T extends Item> extends NodeExtension
+{
+ protected Collection<T> items;
+
+ /**
+ * Construct a request to publish an item to a node.
+ *
+ * @param nodeId The node to publish to
+ * @param toPublish The {@link Item} to publish
+ */
+ public PublishItem(String nodeId, T toPublish)
+ {
+ super(PubSubElementType.PUBLISH, nodeId);
+ items = new ArrayList<T>(1);
+ items.add(toPublish);
+ }
+
+ /**
+ * Construct a request to publish multiple items to a node.
+ *
+ * @param nodeId The node to publish to
+ * @param toPublish The list of {@link Item} to publish
+ */
+ public PublishItem(String nodeId, Collection<T> toPublish)
+ {
+ super(PubSubElementType.PUBLISH, nodeId);
+ items = toPublish;
+ }
+
+ @Override
+ public String toXML()
+ {
+ StringBuilder builder = new StringBuilder("<");
+ builder.append(getElementName());
+ builder.append(" node='");
+ builder.append(getNode());
+ builder.append("'>");
+
+ for (Item item : items)
+ {
+ builder.append(item.toXML());
+ }
+ builder.append("</publish>");
+
+ return builder.toString();
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/PublishModel.java b/src/org/jivesoftware/smackx/pubsub/PublishModel.java new file mode 100644 index 0000000..4b5a851 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/PublishModel.java @@ -0,0 +1,32 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+/**
+ * Determines who may publish to a node. Denotes possible values
+ * for {@link ConfigureForm#setPublishModel(PublishModel)}.
+ *
+ * @author Robin Collier
+ */
+public enum PublishModel
+{
+ /** Only publishers may publish */
+ publishers,
+
+ /** Only subscribers may publish */
+ subscribers,
+
+ /** Anyone may publish */
+ open;
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/RetractItem.java b/src/org/jivesoftware/smackx/pubsub/RetractItem.java new file mode 100644 index 0000000..97db5cc --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/RetractItem.java @@ -0,0 +1,59 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
+
+/**
+ * Represents and item that has been deleted from a node.
+ *
+ * @author Robin Collier
+ */
+public class RetractItem implements PacketExtension
+{
+ private String id;
+
+ /**
+ * Construct a <tt>RetractItem</tt> with the specified id.
+ *
+ * @param itemId The id if the item deleted
+ */
+ public RetractItem(String itemId)
+ {
+ if (itemId == null)
+ throw new IllegalArgumentException("itemId must not be 'null'");
+ id = itemId;
+ }
+
+ public String getId()
+ {
+ return id;
+ }
+
+ public String getElementName()
+ {
+ return "retract";
+ }
+
+ public String getNamespace()
+ {
+ return PubSubNamespace.EVENT.getXmlns();
+ }
+
+ public String toXML()
+ {
+ return "<retract id='" + id + "'/>";
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/SimplePayload.java b/src/org/jivesoftware/smackx/pubsub/SimplePayload.java new file mode 100644 index 0000000..9d114b0 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/SimplePayload.java @@ -0,0 +1,65 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * The default payload representation for {@link Item#getPayload()}. It simply
+ * stores the XML payload as a string.
+ *
+ * @author Robin Collier
+ */
+public class SimplePayload implements PacketExtension
+{
+ private String elemName;
+ private String ns;
+ private String payload;
+
+ /**
+ * Construct a <tt>SimplePayload</tt> object with the specified element name,
+ * namespace and content. The content must be well formed XML.
+ *
+ * @param elementName The root element name (of the payload)
+ * @param namespace The namespace of the payload, null if there is none
+ * @param xmlPayload The payload data
+ */
+ public SimplePayload(String elementName, String namespace, String xmlPayload)
+ {
+ elemName = elementName;
+ payload = xmlPayload;
+ ns = namespace;
+ }
+
+ public String getElementName()
+ {
+ return elemName;
+ }
+
+ public String getNamespace()
+ {
+ return ns;
+ }
+
+ public String toXML()
+ {
+ return payload;
+ }
+
+ @Override
+ public String toString()
+ {
+ return getClass().getName() + "payload [" + toXML() + "]";
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/SubscribeExtension.java b/src/org/jivesoftware/smackx/pubsub/SubscribeExtension.java new file mode 100644 index 0000000..daf8db7 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/SubscribeExtension.java @@ -0,0 +1,60 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+/**
+ * Represents a request to subscribe to a node.
+ *
+ * @author Robin Collier
+ */
+public class SubscribeExtension extends NodeExtension
+{
+ protected String jid;
+
+ public SubscribeExtension(String subscribeJid)
+ {
+ super(PubSubElementType.SUBSCRIBE);
+ jid = subscribeJid;
+ }
+
+ public SubscribeExtension(String subscribeJid, String nodeId)
+ {
+ super(PubSubElementType.SUBSCRIBE, nodeId);
+ jid = subscribeJid;
+ }
+
+ public String getJid()
+ {
+ return jid;
+ }
+
+ @Override
+ public String toXML()
+ {
+ StringBuilder builder = new StringBuilder("<");
+ builder.append(getElementName());
+
+ if (getNode() != null)
+ {
+ builder.append(" node='");
+ builder.append(getNode());
+ builder.append("'");
+ }
+ builder.append(" jid='");
+ builder.append(getJid());
+ builder.append("'/>");
+
+ return builder.toString();
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/SubscribeForm.java b/src/org/jivesoftware/smackx/pubsub/SubscribeForm.java new file mode 100644 index 0000000..53f2606 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/SubscribeForm.java @@ -0,0 +1,241 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.UnknownFormatConversionException;
+
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.packet.DataForm;
+
+/**
+ * A decorator for a {@link Form} to easily enable reading and updating
+ * of subscription options. All operations read or update the underlying {@link DataForm}.
+ *
+ * <p>Unlike the {@link Form}.setAnswer(XXX)} methods, which throw an exception if the field does not
+ * exist, all <b>SubscribeForm.setXXX</b> methods will create the field in the wrapped form
+ * if it does not already exist.
+ *
+ * @author Robin Collier
+ */
+public class SubscribeForm extends Form
+{
+ public SubscribeForm(DataForm configDataForm)
+ {
+ super(configDataForm);
+ }
+
+ public SubscribeForm(Form subscribeOptionsForm)
+ {
+ super(subscribeOptionsForm.getDataFormToSend());
+ }
+
+ public SubscribeForm(FormType formType)
+ {
+ super(formType.toString());
+ }
+
+ /**
+ * Determines if an entity wants to receive notifications.
+ *
+ * @return true if want to receive, false otherwise
+ */
+ public boolean isDeliverOn()
+ {
+ return parseBoolean(getFieldValue(SubscribeOptionFields.deliver));
+ }
+
+ /**
+ * Sets whether an entity wants to receive notifications.
+ *
+ * @param deliverNotifications
+ */
+ public void setDeliverOn(boolean deliverNotifications)
+ {
+ addField(SubscribeOptionFields.deliver, FormField.TYPE_BOOLEAN);
+ setAnswer(SubscribeOptionFields.deliver.getFieldName(), deliverNotifications);
+ }
+
+ /**
+ * Determines if notifications should be delivered as aggregations or not.
+ *
+ * @return true to aggregate, false otherwise
+ */
+ public boolean isDigestOn()
+ {
+ return parseBoolean(getFieldValue(SubscribeOptionFields.digest));
+ }
+
+ /**
+ * Sets whether notifications should be delivered as aggregations or not.
+ *
+ * @param digestOn true to aggregate, false otherwise
+ */
+ public void setDigestOn(boolean digestOn)
+ {
+ addField(SubscribeOptionFields.deliver, FormField.TYPE_BOOLEAN);
+ setAnswer(SubscribeOptionFields.deliver.getFieldName(), digestOn);
+ }
+
+ /**
+ * Gets the minimum number of milliseconds between sending notification digests
+ *
+ * @return The frequency in milliseconds
+ */
+ public int getDigestFrequency()
+ {
+ return Integer.parseInt(getFieldValue(SubscribeOptionFields.digest_frequency));
+ }
+
+ /**
+ * Sets the minimum number of milliseconds between sending notification digests
+ *
+ * @param frequency The frequency in milliseconds
+ */
+ public void setDigestFrequency(int frequency)
+ {
+ addField(SubscribeOptionFields.digest_frequency, FormField.TYPE_TEXT_SINGLE);
+ setAnswer(SubscribeOptionFields.digest_frequency.getFieldName(), frequency);
+ }
+
+ /**
+ * Get the time at which the leased subscription will expire, or has expired.
+ *
+ * @return The expiry date
+ */
+ public Date getExpiry()
+ {
+ String dateTime = getFieldValue(SubscribeOptionFields.expire);
+ try
+ {
+ return StringUtils.parseDate(dateTime);
+ }
+ catch (ParseException e)
+ {
+ UnknownFormatConversionException exc = new UnknownFormatConversionException(dateTime);
+ exc.initCause(e);
+ throw exc;
+ }
+ }
+
+ /**
+ * Sets the time at which the leased subscription will expire, or has expired.
+ *
+ * @param expire The expiry date
+ */
+ public void setExpiry(Date expire)
+ {
+ addField(SubscribeOptionFields.expire, FormField.TYPE_TEXT_SINGLE);
+ setAnswer(SubscribeOptionFields.expire.getFieldName(), StringUtils.formatXEP0082Date(expire));
+ }
+
+ /**
+ * Determines whether the entity wants to receive an XMPP message body in
+ * addition to the payload format.
+ *
+ * @return true to receive the message body, false otherwise
+ */
+ public boolean isIncludeBody()
+ {
+ return parseBoolean(getFieldValue(SubscribeOptionFields.include_body));
+ }
+
+ /**
+ * Sets whether the entity wants to receive an XMPP message body in
+ * addition to the payload format.
+ *
+ * @param include true to receive the message body, false otherwise
+ */
+ public void setIncludeBody(boolean include)
+ {
+ addField(SubscribeOptionFields.include_body, FormField.TYPE_BOOLEAN);
+ setAnswer(SubscribeOptionFields.include_body.getFieldName(), include);
+ }
+
+ /**
+ * Gets the {@link PresenceState} for which an entity wants to receive
+ * notifications.
+ *
+ * @return iterator over the list of states
+ */
+ public Iterator<PresenceState> getShowValues()
+ {
+ ArrayList<PresenceState> result = new ArrayList<PresenceState>(5);
+ Iterator<String > it = getFieldValues(SubscribeOptionFields.show_values);
+
+ while (it.hasNext())
+ {
+ String state = it.next();
+ result.add(PresenceState.valueOf(state));
+ }
+ return result.iterator();
+ }
+
+ /**
+ * Sets the list of {@link PresenceState} for which an entity wants
+ * to receive notifications.
+ *
+ * @param stateValues The list of states
+ */
+ public void setShowValues(Collection<PresenceState> stateValues)
+ {
+ ArrayList<String> values = new ArrayList<String>(stateValues.size());
+
+ for (PresenceState state : stateValues)
+ {
+ values.add(state.toString());
+ }
+ addField(SubscribeOptionFields.show_values, FormField.TYPE_LIST_MULTI);
+ setAnswer(SubscribeOptionFields.show_values.getFieldName(), values);
+ }
+
+
+ static private boolean parseBoolean(String fieldValue)
+ {
+ return ("1".equals(fieldValue) || "true".equals(fieldValue));
+ }
+
+ private String getFieldValue(SubscribeOptionFields field)
+ {
+ FormField formField = getField(field.getFieldName());
+
+ return formField.getValues().next();
+ }
+
+ private Iterator<String> getFieldValues(SubscribeOptionFields field)
+ {
+ FormField formField = getField(field.getFieldName());
+
+ return formField.getValues();
+ }
+
+ private void addField(SubscribeOptionFields nodeField, String type)
+ {
+ String fieldName = nodeField.getFieldName();
+
+ if (getField(fieldName) == null)
+ {
+ FormField field = new FormField(fieldName);
+ field.setType(type);
+ addField(field);
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/SubscribeOptionFields.java b/src/org/jivesoftware/smackx/pubsub/SubscribeOptionFields.java new file mode 100644 index 0000000..dfca601 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/SubscribeOptionFields.java @@ -0,0 +1,99 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.Calendar;
+
+/**
+ * Defines the possible field options for a subscribe options form as defined
+ * by <a href="http://xmpp.org/extensions/xep-0060.html#registrar-formtypes-subscribe">Section 16.4.2</a>.
+ *
+ * @author Robin Collier
+ */
+public enum SubscribeOptionFields
+{
+ /**
+ * Whether an entity wants to receive or disable notifications
+ *
+ * <p><b>Value: boolean</b></p>
+ */
+ deliver,
+
+ /**
+ * Whether an entity wants to receive digests (aggregations) of
+ * notifications or all notifications individually.
+ *
+ * <p><b>Value: boolean</b></p>
+ */
+ digest,
+
+ /**
+ * The minimum number of seconds between sending any two notifications digests
+ *
+ * <p><b>Value: int</b></p>
+ */
+ digest_frequency,
+
+ /**
+ * The DateTime at which a leased subsscription will end ro has ended.
+ *
+ * <p><b>Value: {@link Calendar}</b></p>
+ */
+ expire,
+
+ /**
+ * Whether an entity wants to receive an XMPP message body in addition to
+ * the payload format.
+ *
+ * <p><b>Value: boolean</b></p>
+ */
+ include_body,
+
+ /**
+ * The presence states for which an entity wants to receive notifications.
+ *
+ * <p><b>Value: {@link PresenceState}</b></p>
+ */
+ show_values,
+
+ /**
+ *
+ *
+ * <p><b>Value: </b></p>
+ */
+ subscription_type,
+
+ /**
+ *
+ * <p><b>Value: </b></p>
+ */
+ subscription_depth;
+
+ public String getFieldName()
+ {
+ if (this == show_values)
+ return "pubsub#" + toString().replace('_', '-');
+ return "pubsub#" + toString();
+ }
+
+ static public SubscribeOptionFields valueOfFromElement(String elementName)
+ {
+ String portion = elementName.substring(elementName.lastIndexOf('#' + 1));
+
+ if ("show-values".equals(portion))
+ return show_values;
+ else
+ return valueOf(portion);
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/Subscription.java b/src/org/jivesoftware/smackx/pubsub/Subscription.java new file mode 100644 index 0000000..19ad8a8 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/Subscription.java @@ -0,0 +1,160 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+/**
+ * Represents a subscription to node for both requests and replies.
+ *
+ * @author Robin Collier
+ */
+public class Subscription extends NodeExtension
+{
+ protected String jid;
+ protected String id;
+ protected State state;
+ protected boolean configRequired = false;
+
+ public enum State
+ {
+ subscribed, unconfigured, pending, none
+ }
+
+ /**
+ * Used to constructs a subscription request to the root node with the specified
+ * JID.
+ *
+ * @param subscriptionJid The subscriber JID
+ */
+ public Subscription(String subscriptionJid)
+ {
+ this(subscriptionJid, null, null, null);
+ }
+
+ /**
+ * Used to constructs a subscription request to the specified node with the specified
+ * JID.
+ *
+ * @param subscriptionJid The subscriber JID
+ * @param nodeId The node id
+ */
+ public Subscription(String subscriptionJid, String nodeId)
+ {
+ this(subscriptionJid, nodeId, null, null);
+ }
+
+ /**
+ * Constructs a representation of a subscription reply to the specified node
+ * and JID. The server will have supplied the subscription id and current state.
+ *
+ * @param jid The JID the request was made under
+ * @param nodeId The node subscribed to
+ * @param subscriptionId The id of this subscription
+ * @param state The current state of the subscription
+ */
+ public Subscription(String jid, String nodeId, String subscriptionId, State state)
+ {
+ super(PubSubElementType.SUBSCRIPTION, nodeId);
+ this.jid = jid;
+ id = subscriptionId;
+ this.state = state;
+ }
+
+ /**
+ * Constructs a representation of a subscription reply to the specified node
+ * and JID. The server will have supplied the subscription id and current state
+ * and whether the subscription need to be configured.
+ *
+ * @param jid The JID the request was made under
+ * @param nodeId The node subscribed to
+ * @param subscriptionId The id of this subscription
+ * @param state The current state of the subscription
+ * @param configRequired Is configuration required to complete the subscription
+ */
+ public Subscription(String jid, String nodeId, String subscriptionId, State state, boolean configRequired)
+ {
+ super(PubSubElementType.SUBSCRIPTION, nodeId);
+ this.jid = jid;
+ id = subscriptionId;
+ this.state = state;
+ this.configRequired = configRequired;
+ }
+
+ /**
+ * Gets the JID the subscription is created for
+ *
+ * @return The JID
+ */
+ public String getJid()
+ {
+ return jid;
+ }
+
+ /**
+ * Gets the subscription id
+ *
+ * @return The subscription id
+ */
+ public String getId()
+ {
+ return id;
+ }
+
+ /**
+ * Gets the current subscription state.
+ *
+ * @return Current subscription state
+ */
+ public State getState()
+ {
+ return state;
+ }
+
+ /**
+ * This value is only relevant when the {@link #getState()} is {@link State#unconfigured}
+ *
+ * @return true if configuration is required, false otherwise
+ */
+ public boolean isConfigRequired()
+ {
+ return configRequired;
+ }
+
+ public String toXML()
+ {
+ StringBuilder builder = new StringBuilder("<subscription");
+ appendAttribute(builder, "jid", jid);
+
+ if (getNode() != null)
+ appendAttribute(builder, "node", getNode());
+
+ if (id != null)
+ appendAttribute(builder, "subid", id);
+
+ if (state != null)
+ appendAttribute(builder, "subscription", state.toString());
+
+ builder.append("/>");
+ return builder.toString();
+ }
+
+ private void appendAttribute(StringBuilder builder, String att, String value)
+ {
+ builder.append(" ");
+ builder.append(att);
+ builder.append("='");
+ builder.append(value);
+ builder.append("'");
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/SubscriptionEvent.java b/src/org/jivesoftware/smackx/pubsub/SubscriptionEvent.java new file mode 100644 index 0000000..99f18d5 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/SubscriptionEvent.java @@ -0,0 +1,75 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Base class to represents events that are associated to subscriptions.
+ *
+ * @author Robin Collier
+ */
+abstract public class SubscriptionEvent extends NodeEvent
+{
+ private List<String> subIds = Collections.EMPTY_LIST;
+
+ /**
+ * Construct an event with no subscription id's. This can
+ * occur when there is only one subscription to a node. The
+ * event may or may not report the subscription id along
+ * with the event.
+ *
+ * @param nodeId The id of the node the event came from
+ */
+ protected SubscriptionEvent(String nodeId)
+ {
+ super(nodeId);
+ }
+
+ /**
+ * Construct an event with multiple subscriptions.
+ *
+ * @param nodeId The id of the node the event came from
+ * @param subscriptionIds The list of subscription id's
+ */
+ protected SubscriptionEvent(String nodeId, List<String> subscriptionIds)
+ {
+ super(nodeId);
+
+ if (subscriptionIds != null)
+ subIds = subscriptionIds;
+ }
+
+ /**
+ * Get the subscriptions this event is associated with.
+ *
+ * @return List of subscription id's
+ */
+ public List<String> getSubscriptions()
+ {
+ return Collections.unmodifiableList(subIds);
+ }
+
+ /**
+ * Set the list of subscription id's for this event.
+ *
+ * @param subscriptionIds The list of subscription id's
+ */
+ protected void setSubscriptions(List<String> subscriptionIds)
+ {
+ if (subscriptionIds != null)
+ subIds = subscriptionIds;
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/SubscriptionsExtension.java b/src/org/jivesoftware/smackx/pubsub/SubscriptionsExtension.java new file mode 100644 index 0000000..a28cbe2 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/SubscriptionsExtension.java @@ -0,0 +1,96 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents the element holding the list of subscription elements.
+ *
+ * @author Robin Collier
+ */
+public class SubscriptionsExtension extends NodeExtension
+{
+ protected List<Subscription> items = Collections.EMPTY_LIST;
+
+ /**
+ * Subscriptions to the root node
+ *
+ * @param subList The list of subscriptions
+ */
+ public SubscriptionsExtension(List<Subscription> subList)
+ {
+ super(PubSubElementType.SUBSCRIPTIONS);
+
+ if (subList != null)
+ items = subList;
+ }
+
+ /**
+ * Subscriptions to the specified node.
+ *
+ * @param nodeId The node subscribed to
+ * @param subList The list of subscriptions
+ */
+ public SubscriptionsExtension(String nodeId, List<Subscription> subList)
+ {
+ super(PubSubElementType.SUBSCRIPTIONS, nodeId);
+
+ if (subList != null)
+ items = subList;
+ }
+
+ /**
+ * Gets the list of subscriptions.
+ *
+ * @return List of subscriptions
+ */
+ public List<Subscription> getSubscriptions()
+ {
+ return items;
+ }
+
+ @Override
+ public String toXML()
+ {
+ if ((items == null) || (items.size() == 0))
+ {
+ return super.toXML();
+ }
+ else
+ {
+ StringBuilder builder = new StringBuilder("<");
+ builder.append(getElementName());
+
+ if (getNode() != null)
+ {
+ builder.append(" node='");
+ builder.append(getNode());
+ builder.append("'");
+ }
+ builder.append(">");
+
+ for (Subscription item : items)
+ {
+ builder.append(item.toXML());
+ }
+
+ builder.append("</");
+ builder.append(getElementName());
+ builder.append(">");
+ return builder.toString();
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/UnsubscribeExtension.java b/src/org/jivesoftware/smackx/pubsub/UnsubscribeExtension.java new file mode 100644 index 0000000..ac14c60 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/UnsubscribeExtension.java @@ -0,0 +1,73 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub;
+
+import org.jivesoftware.smackx.pubsub.util.XmlUtils;
+
+
+/**
+ * Represents an unsubscribe element.
+ *
+ * @author Robin Collier
+ */
+public class UnsubscribeExtension extends NodeExtension
+{
+ protected String jid;
+ protected String id;
+
+ public UnsubscribeExtension(String subscriptionJid)
+ {
+ this(subscriptionJid, null, null);
+ }
+
+ public UnsubscribeExtension(String subscriptionJid, String nodeId)
+ {
+ this(subscriptionJid, nodeId, null);
+ }
+
+ public UnsubscribeExtension(String jid, String nodeId, String subscriptionId)
+ {
+ super(PubSubElementType.UNSUBSCRIBE, nodeId);
+ this.jid = jid;
+ id = subscriptionId;
+ }
+
+ public String getJid()
+ {
+ return jid;
+ }
+
+ public String getId()
+ {
+ return id;
+ }
+
+ @Override
+ public String toXML()
+ {
+ StringBuilder builder = new StringBuilder("<");
+ builder.append(getElementName());
+ XmlUtils.appendAttribute(builder, "jid", jid);
+
+ if (getNode() != null)
+ XmlUtils.appendAttribute(builder, "node", getNode());
+
+ if (id != null)
+ XmlUtils.appendAttribute(builder, "subid", id);
+
+ builder.append("/>");
+ return builder.toString();
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/listener/ItemDeleteListener.java b/src/org/jivesoftware/smackx/pubsub/listener/ItemDeleteListener.java new file mode 100644 index 0000000..d228e8f --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/listener/ItemDeleteListener.java @@ -0,0 +1,41 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.listener;
+
+import org.jivesoftware.smackx.pubsub.ItemDeleteEvent;
+import org.jivesoftware.smackx.pubsub.LeafNode;
+
+/**
+ * Defines the listener for item deletion events from a node.
+ *
+ * @see LeafNode#addItemDeleteListener(ItemDeleteListener)
+ *
+ * @author Robin Collier
+ */
+public interface ItemDeleteListener
+{
+ /**
+ * Called when items are deleted from a node the listener is
+ * registered with.
+ *
+ * @param items The event with item deletion details
+ */
+ void handleDeletedItems(ItemDeleteEvent items);
+
+ /**
+ * Called when <b>all</b> items are deleted from a node the listener is
+ * registered with.
+ */
+ void handlePurge();
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/listener/ItemEventListener.java b/src/org/jivesoftware/smackx/pubsub/listener/ItemEventListener.java new file mode 100644 index 0000000..714b2c0 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/listener/ItemEventListener.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.
+ */
+package org.jivesoftware.smackx.pubsub.listener;
+
+import org.jivesoftware.smackx.pubsub.Item;
+import org.jivesoftware.smackx.pubsub.ItemPublishEvent;
+import org.jivesoftware.smackx.pubsub.LeafNode;
+
+/**
+ * Defines the listener for items being published to a node.
+ *
+ * @see LeafNode#addItemEventListener(ItemEventListener)
+ *
+ * @author Robin Collier
+ */
+public interface ItemEventListener <T extends Item>
+{
+ /**
+ * Called whenever an item is published to the node the listener
+ * is registered with.
+ *
+ * @param items The publishing details.
+ */
+ void handlePublishedItems(ItemPublishEvent<T> items);
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/listener/NodeConfigListener.java b/src/org/jivesoftware/smackx/pubsub/listener/NodeConfigListener.java new file mode 100644 index 0000000..39db5a5 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/listener/NodeConfigListener.java @@ -0,0 +1,35 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.listener;
+
+import org.jivesoftware.smackx.pubsub.ConfigurationEvent;
+import org.jivesoftware.smackx.pubsub.LeafNode;
+
+/**
+ * Defines the listener for a node being configured.
+ *
+ * @see LeafNode#addConfigurationListener(NodeConfigListener)
+ *
+ * @author Robin Collier
+ */
+public interface NodeConfigListener
+{
+ /**
+ * Called whenever the node the listener
+ * is registered with is configured.
+ *
+ * @param config The configuration details.
+ */
+ void handleNodeConfiguration(ConfigurationEvent config);
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/packet/PubSub.java b/src/org/jivesoftware/smackx/pubsub/packet/PubSub.java new file mode 100644 index 0000000..5aa4865 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/packet/PubSub.java @@ -0,0 +1,106 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.pubsub.PubSubElementType;
+
+/**
+ * The standard PubSub extension of an {@link IQ} packet. This is the topmost
+ * element of all pubsub requests and replies as defined in the <a href="http://xmpp.org/extensions/xep-0060">Publish-Subscribe</a>
+ * specification.
+ *
+ * @author Robin Collier
+ */
+public class PubSub extends IQ
+{
+ private PubSubNamespace ns = PubSubNamespace.BASIC;
+
+ /**
+ * 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 "pubsub";
+ }
+
+ /**
+ * Returns the XML namespace of the extension sub-packet root element.
+ * According the specification the namespace is
+ * http://jabber.org/protocol/pubsub with a specific fragment depending
+ * on the request. The namespace is defined at <a href="http://xmpp.org/registrar/namespaces.html">XMPP Registrar</a> at
+ *
+ * The default value has no fragment.
+ *
+ * @return the XML namespace of the packet extension.
+ */
+ public String getNamespace()
+ {
+ return ns.getXmlns();
+ }
+
+ /**
+ * Set the namespace for the packet if it something other than the default
+ * case of {@link PubSubNamespace#BASIC}. The {@link #getNamespace()} method will return
+ * the result of calling {@link PubSubNamespace#getXmlns()} on the specified enum.
+ *
+ * @param ns - The new value for the namespace.
+ */
+ public void setPubSubNamespace(PubSubNamespace ns)
+ {
+ this.ns = ns;
+ }
+
+ public PacketExtension getExtension(PubSubElementType elem)
+ {
+ return getExtension(elem.getElementName(), elem.getNamespace().getXmlns());
+ }
+
+ /**
+ * Returns the current value of the namespace. The {@link #getNamespace()} method will return
+ * the result of calling {@link PubSubNamespace#getXmlns()} this value.
+ *
+ * @return The current value of the namespace.
+ */
+ public PubSubNamespace getPubSubNamespace()
+ {
+ return ns;
+ }
+ /**
+ * Returns the XML representation of a pubsub element according the specification.
+ *
+ * The XML representation will be inside of an iq packet like
+ * in the following example:
+ * <pre>
+ * <iq type='set' id="MlIpV-4" to="pubsub.gato.home" from="gato3@gato.home/Smack">
+ * <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ * :
+ * Specific request extension
+ * :
+ * </pubsub>
+ * </iq>
+ * </pre>
+ *
+ */
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\">");
+ buf.append(getExtensionsXML());
+ buf.append("</").append(getElementName()).append(">");
+ return buf.toString();
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/packet/PubSubNamespace.java b/src/org/jivesoftware/smackx/pubsub/packet/PubSubNamespace.java new file mode 100644 index 0000000..eecf959 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/packet/PubSubNamespace.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.smackx.pubsub.packet;
+
+/**
+ * Defines all the valid namespaces that are used with the {@link PubSub} packet
+ * as defined by the specification.
+ *
+ * @author Robin Collier
+ */
+public enum PubSubNamespace
+{
+ BASIC(null),
+ ERROR("errors"),
+ EVENT("event"),
+ OWNER("owner");
+
+ private String fragment;
+
+ private PubSubNamespace(String fragment)
+ {
+ this.fragment = fragment;
+ }
+
+ public String getXmlns()
+ {
+ String ns = "http://jabber.org/protocol/pubsub";
+
+ if (fragment != null)
+ ns += '#' + fragment;
+
+ return ns;
+ }
+
+ public String getFragment()
+ {
+ return fragment;
+ }
+
+ public static PubSubNamespace valueOfFromXmlns(String ns)
+ {
+ int index = ns.lastIndexOf('#');
+
+ if (index != -1)
+ {
+ String suffix = ns.substring(ns.lastIndexOf('#')+1);
+ return valueOf(suffix.toUpperCase());
+ }
+ else
+ return BASIC;
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/packet/SyncPacketSend.java b/src/org/jivesoftware/smackx/pubsub/packet/SyncPacketSend.java new file mode 100644 index 0000000..080129b --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/packet/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.smackx.pubsub.packet;
+
+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/smackx/pubsub/provider/AffiliationProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/AffiliationProvider.java new file mode 100644 index 0000000..892eec6 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/provider/AffiliationProvider.java @@ -0,0 +1,37 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.provider;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
+import org.jivesoftware.smackx.pubsub.Affiliation;
+
+/**
+ * Parses the affiliation element out of the reply stanza from the server
+ * as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-pubsub">affiliation schema</a>.
+ *
+ * @author Robin Collier
+ */
+public class AffiliationProvider extends EmbeddedExtensionProvider
+{
+ @Override
+ protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
+ {
+ return new Affiliation(attributeMap.get("jid"), attributeMap.get("node"), Affiliation.Type.valueOf(attributeMap.get("affiliation")));
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/provider/AffiliationsProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/AffiliationsProvider.java new file mode 100644 index 0000000..ee7af05 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/provider/AffiliationsProvider.java @@ -0,0 +1,38 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jivesoftware.smackx.pubsub.provider;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
+import org.jivesoftware.smackx.pubsub.Affiliation;
+import org.jivesoftware.smackx.pubsub.AffiliationsExtension;
+
+/**
+ * Parses the affiliations element out of the reply stanza from the server
+ * as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-pubsub">affiliation schema</a>.
+ *
+ * @author Robin Collier
+ */public class AffiliationsProvider extends EmbeddedExtensionProvider
+{
+ @Override
+ protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
+ {
+ return new AffiliationsExtension(attributeMap.get("node"), (List<Affiliation>)content);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/provider/ConfigEventProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/ConfigEventProvider.java new file mode 100644 index 0000000..30e3017 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/provider/ConfigEventProvider.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.smackx.pubsub.provider;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
+import org.jivesoftware.smackx.pubsub.ConfigurationEvent;
+import org.jivesoftware.smackx.pubsub.ConfigureForm;
+
+/**
+ * Parses the node configuration element out of the message event stanza from
+ * the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-event">configuration schema</a>.
+ *
+ * @author Robin Collier
+ */
+public class ConfigEventProvider extends EmbeddedExtensionProvider
+{
+ @Override
+ protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attMap, List<? extends PacketExtension> content)
+ {
+ if (content.size() == 0)
+ return new ConfigurationEvent(attMap.get("node"));
+ else
+ return new ConfigurationEvent(attMap.get("node"), new ConfigureForm((DataForm)content.iterator().next()));
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/provider/EventProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/EventProvider.java new file mode 100644 index 0000000..ef5671e --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/provider/EventProvider.java @@ -0,0 +1,38 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.provider;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
+import org.jivesoftware.smackx.pubsub.EventElement;
+import org.jivesoftware.smackx.pubsub.EventElementType;
+import org.jivesoftware.smackx.pubsub.NodeExtension;
+
+/**
+ * Parses the event element out of the message stanza from
+ * the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-event">event schema</a>.
+ *
+ * @author Robin Collier
+ */
+public class EventProvider extends EmbeddedExtensionProvider
+{
+ @Override
+ protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attMap, List<? extends PacketExtension> content)
+ {
+ return new EventElement(EventElementType.valueOf(content.get(0).getElementName()), (NodeExtension)content.get(0));
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/provider/FormNodeProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/FormNodeProvider.java new file mode 100644 index 0000000..da75b24 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/provider/FormNodeProvider.java @@ -0,0 +1,39 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.provider;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
+import org.jivesoftware.smackx.pubsub.FormNode;
+import org.jivesoftware.smackx.pubsub.FormNodeType;
+
+/**
+ * Parses one of several elements used in pubsub that contain a form of some kind as a child element. The
+ * elements and namespaces supported is defined in {@link FormNodeType}.
+ *
+ * @author Robin Collier
+ */
+public class FormNodeProvider extends EmbeddedExtensionProvider
+{
+ @Override
+ protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
+ {
+ return new FormNode(FormNodeType.valueOfFromElementName(currentElement, currentNamespace), attributeMap.get("node"), new Form((DataForm)content.iterator().next()));
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java new file mode 100644 index 0000000..a6b8694 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java @@ -0,0 +1,92 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smack.provider.ProviderManager;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.pubsub.Item;
+import org.jivesoftware.smackx.pubsub.PayloadItem;
+import org.jivesoftware.smackx.pubsub.SimplePayload;
+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Parses an <b>item</b> element as is defined in both the {@link PubSubNamespace#BASIC} and {@link PubSubNamespace#EVENT}
+ * namespaces. To parse the item contents, it will use whatever {@link PacketExtensionProvider} is registered in
+ * <b>smack.providers</b> for its element name and namespace. If no provider is registered, it will return a {@link SimplePayload}.
+ *
+ * @author Robin Collier
+ */
+public class ItemProvider implements PacketExtensionProvider {
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ String id = parser.getAttributeValue(null, "id");
+ String node = parser.getAttributeValue(null, "node");
+ String elem = parser.getName();
+
+ int tag = parser.next();
+
+ if (tag == XmlPullParser.END_TAG) {
+ return new Item(id, node);
+ } else {
+ String payloadElemName = parser.getName();
+ String payloadNS = parser.getNamespace();
+
+ if (ProviderManager.getInstance().getExtensionProvider(payloadElemName, payloadNS) == null) {
+ StringBuilder payloadText = new StringBuilder();
+ boolean done = false;
+ boolean isEmptyElement = false;
+
+ // Parse custom payload
+ while (!done) {
+ if (tag == XmlPullParser.END_TAG && parser.getName().equals(elem)) {
+ done = true;
+ } else if (parser.getEventType() == XmlPullParser.START_TAG) {
+ payloadText.append("<").append(parser.getName());
+ if (parser.getName().equals(payloadElemName) && (!"".equals(payloadNS))) {
+ payloadText.append(" xmlns=\"").append(payloadNS).append("\"");
+ }
+ int n = parser.getAttributeCount();
+ for (int i = 0; i < n; i++) {
+ payloadText.append(" ").append(parser.getAttributeName(i)).append("=\"")
+ .append(parser.getAttributeValue(i)).append("\"");
+ }
+ if (parser.isEmptyElementTag()) {
+ payloadText.append("/>");
+ isEmptyElement = true;
+ } else {
+ payloadText.append(">");
+ }
+ } else if (parser.getEventType() == XmlPullParser.END_TAG) {
+ if (isEmptyElement) {
+ isEmptyElement = false;
+ } else {
+ payloadText.append("</").append(parser.getName()).append(">");
+ }
+ } else if (parser.getEventType() == XmlPullParser.TEXT) {
+ payloadText.append(parser.getText());
+ }
+
+ tag = parser.next();
+ }
+ return new PayloadItem<SimplePayload>(id, node, new SimplePayload(payloadElemName, payloadNS,
+ payloadText.toString()));
+ } else {
+ return new PayloadItem<PacketExtension>(id, node, PacketParserUtils.parsePacketExtension(
+ payloadElemName, payloadNS, parser));
+ }
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/provider/ItemsProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/ItemsProvider.java new file mode 100644 index 0000000..01cb9d4 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/provider/ItemsProvider.java @@ -0,0 +1,38 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.provider;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
+import org.jivesoftware.smackx.pubsub.ItemsExtension;
+
+/**
+ * Parses the <b>items</b> element out of the message event stanza from
+ * the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-event">items schema</a>.
+ *
+ * @author Robin Collier
+ */
+public class ItemsProvider extends EmbeddedExtensionProvider
+{
+
+ @Override
+ protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
+ {
+ return new ItemsExtension(ItemsExtension.ItemsElementType.items, attributeMap.get("node"), content);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/provider/PubSubProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/PubSubProvider.java new file mode 100644 index 0000000..742f219 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/provider/PubSubProvider.java @@ -0,0 +1,62 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.pubsub.packet.PubSub;
+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Parses the root pubsub packet extensions of the {@link IQ} packet and returns
+ * a {@link PubSub} instance.
+ *
+ * @author Robin Collier
+ */
+public class PubSubProvider implements IQProvider
+{
+ public IQ parseIQ(XmlPullParser parser) throws Exception
+ {
+ PubSub pubsub = new PubSub();
+ String namespace = parser.getNamespace();
+ pubsub.setPubSubNamespace(PubSubNamespace.valueOfFromXmlns(namespace));
+ boolean done = false;
+
+ while (!done)
+ {
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG)
+ {
+ PacketExtension ext = PacketParserUtils.parsePacketExtension(parser.getName(), namespace, parser);
+
+ if (ext != null)
+ {
+ pubsub.addExtension(ext);
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG)
+ {
+ if (parser.getName().equals("pubsub"))
+ {
+ done = true;
+ }
+ }
+ }
+ return pubsub;
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/provider/RetractEventProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/RetractEventProvider.java new file mode 100644 index 0000000..8fa3337 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/provider/RetractEventProvider.java @@ -0,0 +1,38 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.provider;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
+import org.jivesoftware.smackx.pubsub.RetractItem;
+
+/**
+ * Parses the <b>retract</b> element out of the message event stanza from
+ * the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-event">retract schema</a>.
+ * This element is a child of the <b>items</b> element.
+ *
+ * @author Robin Collier
+ */
+public class RetractEventProvider extends EmbeddedExtensionProvider
+{
+ @Override
+ protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
+ {
+ return new RetractItem(attributeMap.get("id"));
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/provider/SimpleNodeProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/SimpleNodeProvider.java new file mode 100644 index 0000000..d2b7d30 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/provider/SimpleNodeProvider.java @@ -0,0 +1,37 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.provider;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
+import org.jivesoftware.smackx.pubsub.NodeExtension;
+import org.jivesoftware.smackx.pubsub.PubSubElementType;
+
+/**
+ * Parses simple elements that only contain a <b>node</b> attribute. This is common amongst many of the
+ * elements defined in the pubsub specification. For this common case a {@link NodeExtension} is returned.
+ *
+ * @author Robin Collier
+ */
+public class SimpleNodeProvider extends EmbeddedExtensionProvider
+{
+ @Override
+ protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
+ {
+ return new NodeExtension(PubSubElementType.valueOfFromElemName(currentElement, currentNamespace), attributeMap.get("node"));
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/provider/SubscriptionProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/SubscriptionProvider.java new file mode 100644 index 0000000..eccbe08 --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/provider/SubscriptionProvider.java @@ -0,0 +1,52 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.pubsub.Subscription;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Parses the <b>subscription</b> element out of the pubsub IQ message from
+ * the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-pubsub">subscription schema</a>.
+ *
+ * @author Robin Collier
+ */
+public class SubscriptionProvider implements PacketExtensionProvider
+{
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception
+ {
+ String jid = parser.getAttributeValue(null, "jid");
+ String nodeId = parser.getAttributeValue(null, "node");
+ String subId = parser.getAttributeValue(null, "subid");
+ String state = parser.getAttributeValue(null, "subscription");
+ boolean isRequired = false;
+
+ int tag = parser.next();
+
+ if ((tag == XmlPullParser.START_TAG) && parser.getName().equals("subscribe-options"))
+ {
+ tag = parser.next();
+
+ if ((tag == XmlPullParser.START_TAG) && parser.getName().equals("required"))
+ isRequired = true;
+
+ while (parser.next() != XmlPullParser.END_TAG && parser.getName() != "subscribe-options");
+ }
+ while (parser.getEventType() != XmlPullParser.END_TAG) parser.next();
+ return new Subscription(jid, nodeId, subId, (state == null ? null : Subscription.State.valueOf(state)), isRequired);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/provider/SubscriptionsProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/SubscriptionsProvider.java new file mode 100644 index 0000000..94dc61d --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/provider/SubscriptionsProvider.java @@ -0,0 +1,38 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.provider;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;
+import org.jivesoftware.smackx.pubsub.Subscription;
+import org.jivesoftware.smackx.pubsub.SubscriptionsExtension;
+
+/**
+ * Parses the <b>subscriptions</b> element out of the pubsub IQ message from
+ * the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-pubsub">subscriptions schema</a>.
+ *
+ * @author Robin Collier
+ */
+public class SubscriptionsProvider extends EmbeddedExtensionProvider
+{
+ @Override
+ protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)
+ {
+ return new SubscriptionsExtension(attributeMap.get("node"), (List<Subscription>)content);
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/util/NodeUtils.java b/src/org/jivesoftware/smackx/pubsub/util/NodeUtils.java new file mode 100644 index 0000000..414601f --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/util/NodeUtils.java @@ -0,0 +1,43 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.util;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.pubsub.ConfigureForm;
+import org.jivesoftware.smackx.pubsub.FormNode;
+import org.jivesoftware.smackx.pubsub.PubSubElementType;
+
+/**
+ * Utility for extracting information from packets.
+ *
+ * @author Robin Collier
+ */
+public class NodeUtils
+{
+ /**
+ * Get a {@link ConfigureForm} from a packet.
+ *
+ * @param packet
+ * @param elem
+ * @return The configuration form
+ */
+ public static ConfigureForm getFormFromPacket(Packet packet, PubSubElementType elem)
+ {
+ FormNode config = (FormNode)packet.getExtension(elem.getElementName(), elem.getNamespace().getXmlns());
+ Form formReply = config.getForm();
+ return new ConfigureForm(formReply);
+
+ }
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/util/XmlUtils.java b/src/org/jivesoftware/smackx/pubsub/util/XmlUtils.java new file mode 100644 index 0000000..8e4a77c --- /dev/null +++ b/src/org/jivesoftware/smackx/pubsub/util/XmlUtils.java @@ -0,0 +1,35 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.pubsub.util;
+
+import java.io.StringReader;
+
+/**
+ * Simple utility for pretty printing xml.
+ *
+ * @author Robin Collier
+ */
+public class XmlUtils
+{
+
+ static public void appendAttribute(StringBuilder builder, String att, String value)
+ {
+ builder.append(" ");
+ builder.append(att);
+ builder.append("='");
+ builder.append(value);
+ builder.append("'");
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/receipts/DeliveryReceipt.java b/src/org/jivesoftware/smackx/receipts/DeliveryReceipt.java new file mode 100644 index 0000000..9020556 --- /dev/null +++ b/src/org/jivesoftware/smackx/receipts/DeliveryReceipt.java @@ -0,0 +1,77 @@ +/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.receipts;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.EmbeddedExtensionProvider;
+
+/**
+ * Represents a <b>message delivery receipt</b> entry as specified by
+ * <a href="http://xmpp.org/extensions/xep-0184.html">Message Delivery Receipts</a>.
+ *
+ * @author Georg Lukas
+ */
+public class DeliveryReceipt implements PacketExtension
+{
+ public static final String NAMESPACE = "urn:xmpp:receipts";
+ public static final String ELEMENT = "received";
+
+ private String id; /// original ID of the delivered message
+
+ public DeliveryReceipt(String id)
+ {
+ this.id = id;
+ }
+
+ public String getId()
+ {
+ return id;
+ }
+
+ @Override
+ public String getElementName()
+ {
+ return ELEMENT;
+ }
+
+ @Override
+ public String getNamespace()
+ {
+ return NAMESPACE;
+ }
+
+ @Override
+ public String toXML()
+ {
+ return "<received xmlns='" + NAMESPACE + "' id='" + id + "'/>";
+ }
+
+ /**
+ * This Provider parses and returns DeliveryReceipt packets.
+ */
+ public static class Provider extends EmbeddedExtensionProvider
+ {
+
+ @Override
+ protected PacketExtension createReturnExtension(String currentElement, String currentNamespace,
+ Map<String, String> attributeMap, List<? extends PacketExtension> content)
+ {
+ return new DeliveryReceipt(attributeMap.get("id"));
+ }
+
+ }
+}
diff --git a/src/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java b/src/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java new file mode 100644 index 0000000..125b87e --- /dev/null +++ b/src/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java @@ -0,0 +1,202 @@ +/** + * Copyright 2013 Georg Lukas + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.receipts; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.PacketExtensionFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; + +/** + * Manager for XEP-0184: Message Delivery Receipts. This class implements + * the manager for {@link DeliveryReceipt} support, enabling and disabling of + * automatic DeliveryReceipt transmission. + * + * @author Georg Lukas + */ +public class DeliveryReceiptManager implements PacketListener { + + private static Map<Connection, DeliveryReceiptManager> instances = + Collections.synchronizedMap(new WeakHashMap<Connection, DeliveryReceiptManager>()); + + static { + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(Connection connection) { + new DeliveryReceiptManager(connection); + } + }); + } + + private Connection connection; + private boolean auto_receipts_enabled = false; + private Set<ReceiptReceivedListener> receiptReceivedListeners = Collections + .synchronizedSet(new HashSet<ReceiptReceivedListener>()); + + private DeliveryReceiptManager(Connection connection) { + ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); + sdm.addFeature(DeliveryReceipt.NAMESPACE); + this.connection = connection; + instances.put(connection, this); + + // register listener for delivery receipts and requests + connection.addPacketListener(this, new PacketExtensionFilter(DeliveryReceipt.NAMESPACE)); + } + + /** + * Obtain the DeliveryReceiptManager responsible for a connection. + * + * @param connection the connection object. + * + * @return the DeliveryReceiptManager instance for the given connection + */ + synchronized public static DeliveryReceiptManager getInstanceFor(Connection connection) { + DeliveryReceiptManager receiptManager = instances.get(connection); + + if (receiptManager == null) { + receiptManager = new DeliveryReceiptManager(connection); + } + + return receiptManager; + } + + /** + * Returns true if Delivery Receipts are supported by a given JID + * + * @param jid + * @return true if supported + */ + public boolean isSupported(String jid) { + try { + DiscoverInfo result = + ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid); + return result.containsFeature(DeliveryReceipt.NAMESPACE); + } + catch (XMPPException e) { + return false; + } + } + + // handle incoming receipts and receipt requests + @Override + public void processPacket(Packet packet) { + DeliveryReceipt dr = (DeliveryReceipt)packet.getExtension( + DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE); + if (dr != null) { + // notify listeners of incoming receipt + for (ReceiptReceivedListener l : receiptReceivedListeners) { + l.onReceiptReceived(packet.getFrom(), packet.getTo(), dr.getId()); + } + + } + + // if enabled, automatically send a receipt + if (auto_receipts_enabled) { + DeliveryReceiptRequest drr = (DeliveryReceiptRequest)packet.getExtension( + DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE); + if (drr != null) { + Message ack = new Message(packet.getFrom(), Message.Type.normal); + ack.addExtension(new DeliveryReceipt(packet.getPacketID())); + connection.sendPacket(ack); + } + } + } + + /** + * Configure whether the {@link DeliveryReceiptManager} should automatically + * reply to incoming {@link DeliveryReceipt}s. By default, this feature is off. + * + * @param new_state whether automatic transmission of + * DeliveryReceipts should be enabled or disabled + */ + public void setAutoReceiptsEnabled(boolean new_state) { + auto_receipts_enabled = new_state; + } + + /** + * Helper method to enable automatic DeliveryReceipt transmission. + */ + public void enableAutoReceipts() { + setAutoReceiptsEnabled(true); + } + + /** + * Helper method to disable automatic DeliveryReceipt transmission. + */ + public void disableAutoReceipts() { + setAutoReceiptsEnabled(false); + } + + /** + * Check if AutoReceipts are enabled on this connection. + */ + public boolean getAutoReceiptsEnabled() { + return this.auto_receipts_enabled; + } + + /** + * Get informed about incoming delivery receipts with a {@link ReceiptReceivedListener}. + * + * @param listener the listener to be informed about new receipts + */ + public void addReceiptReceivedListener(ReceiptReceivedListener listener) { + receiptReceivedListeners.add(listener); + } + + /** + * Stop getting informed about incoming delivery receipts. + * + * @param listener the listener to be removed + */ + public void removeReceiptReceivedListener(ReceiptReceivedListener listener) { + receiptReceivedListeners.remove(listener); + } + + /** + * Test if a packet requires a delivery receipt. + * + * @param p Packet object to check for a DeliveryReceiptRequest + * + * @return true if a delivery receipt was requested + */ + public static boolean hasDeliveryReceiptRequest(Packet p) { + return (p.getExtension(DeliveryReceiptRequest.ELEMENT, + DeliveryReceipt.NAMESPACE) != null); + } + + /** + * Add a delivery receipt request to an outgoing packet. + * + * Only message packets may contain receipt requests as of XEP-0184, + * therefore only allow Message as the parameter type. + * + * @param m Message object to add a request to + */ + public static void addDeliveryReceiptRequest(Message m) { + m.addExtension(new DeliveryReceiptRequest()); + } +} diff --git a/src/org/jivesoftware/smackx/receipts/DeliveryReceiptRequest.java b/src/org/jivesoftware/smackx/receipts/DeliveryReceiptRequest.java new file mode 100644 index 0000000..1b5ed3b --- /dev/null +++ b/src/org/jivesoftware/smackx/receipts/DeliveryReceiptRequest.java @@ -0,0 +1,54 @@ +/*
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smackx.receipts;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Represents a <b>message delivery receipt request</b> entry as specified by
+ * <a href="http://xmpp.org/extensions/xep-0184.html">Message Delivery Receipts</a>.
+ *
+ * @author Georg Lukas
+ */
+public class DeliveryReceiptRequest implements PacketExtension
+{
+ public static final String ELEMENT = "request";
+
+ public String getElementName()
+ {
+ return ELEMENT;
+ }
+
+ public String getNamespace()
+ {
+ return DeliveryReceipt.NAMESPACE;
+ }
+
+ public String toXML()
+ {
+ return "<request xmlns='" + DeliveryReceipt.NAMESPACE + "'/>";
+ }
+
+ /**
+ * This Provider parses and returns DeliveryReceiptRequest packets.
+ */
+ public static class Provider implements PacketExtensionProvider {
+ @Override
+ public PacketExtension parseExtension(XmlPullParser parser) {
+ return new DeliveryReceiptRequest();
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/receipts/ReceiptReceivedListener.java b/src/org/jivesoftware/smackx/receipts/ReceiptReceivedListener.java new file mode 100644 index 0000000..3183113 --- /dev/null +++ b/src/org/jivesoftware/smackx/receipts/ReceiptReceivedListener.java @@ -0,0 +1,26 @@ +/**
+ * Copyright 2013 Georg Lukas
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jivesoftware.smackx.receipts;
+
+/**
+ * Interface for received receipt notifications.
+ *
+ * Implement this and add a listener to get notified.
+ */
+public interface ReceiptReceivedListener {
+ void onReceiptReceived(String fromJid, String toJid, String receiptId);
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/search/SimpleUserSearch.java b/src/org/jivesoftware/smackx/search/SimpleUserSearch.java new file mode 100644 index 0000000..74a70f0 --- /dev/null +++ b/src/org/jivesoftware/smackx/search/SimpleUserSearch.java @@ -0,0 +1,151 @@ +/** + * 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.smackx.search; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.ReportedData; +import org.xmlpull.v1.XmlPullParser; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * SimpleUserSearch is used to support the non-dataform type of JEP 55. This provides + * the mechanism for allowing always type ReportedData to be returned by any search result, + * regardless of the form of the data returned from the server. + * + * @author Derek DeMoro + */ +class SimpleUserSearch extends IQ { + + private Form form; + private ReportedData data; + + public void setForm(Form form) { + this.form = form; + } + + public ReportedData getReportedData() { + return data; + } + + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<query xmlns=\"jabber:iq:search\">"); + buf.append(getItemsToSearch()); + buf.append("</query>"); + return buf.toString(); + } + + private String getItemsToSearch() { + StringBuilder buf = new StringBuilder(); + + if (form == null) { + form = Form.getFormFrom(this); + } + + if (form == null) { + return ""; + } + + Iterator<FormField> fields = form.getFields(); + while (fields.hasNext()) { + FormField field = fields.next(); + String name = field.getVariable(); + String value = getSingleValue(field); + if (value.trim().length() > 0) { + buf.append("<").append(name).append(">").append(value).append("</").append(name).append(">"); + } + } + + return buf.toString(); + } + + private static String getSingleValue(FormField formField) { + Iterator<String> values = formField.getValues(); + while (values.hasNext()) { + return values.next(); + } + return ""; + } + + protected void parseItems(XmlPullParser parser) throws Exception { + ReportedData data = new ReportedData(); + data.addColumn(new ReportedData.Column("JID", "jid", "text-single")); + + boolean done = false; + + List<ReportedData.Field> fields = new ArrayList<ReportedData.Field>(); + while (!done) { + if (parser.getAttributeCount() > 0) { + String jid = parser.getAttributeValue("", "jid"); + List<String> valueList = new ArrayList<String>(); + valueList.add(jid); + ReportedData.Field field = new ReportedData.Field("jid", valueList); + fields.add(field); + } + + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG && parser.getName().equals("item")) { + fields = new ArrayList<ReportedData.Field>(); + } + else if (eventType == XmlPullParser.END_TAG && parser.getName().equals("item")) { + ReportedData.Row row = new ReportedData.Row(fields); + data.addRow(row); + } + else if (eventType == XmlPullParser.START_TAG) { + String name = parser.getName(); + String value = parser.nextText(); + + List<String> valueList = new ArrayList<String>(); + valueList.add(value); + ReportedData.Field field = new ReportedData.Field(name, valueList); + fields.add(field); + + boolean exists = false; + Iterator<ReportedData.Column> cols = data.getColumns(); + while (cols.hasNext()) { + ReportedData.Column column = cols.next(); + if (column.getVariable().equals(name)) { + exists = true; + } + } + + // Column name should be the same + if (!exists) { + ReportedData.Column column = new ReportedData.Column(name, name, "text-single"); + data.addColumn(column); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + + } + + this.data = data; + } + + +} diff --git a/src/org/jivesoftware/smackx/search/UserSearch.java b/src/org/jivesoftware/smackx/search/UserSearch.java new file mode 100644 index 0000000..781dd9a --- /dev/null +++ b/src/org/jivesoftware/smackx/search/UserSearch.java @@ -0,0 +1,255 @@ +/** + * 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.smackx.search; + +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.PacketIDFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.ReportedData; +import org.jivesoftware.smackx.packet.DataForm; +import org.xmlpull.v1.XmlPullParser; + +/** + * Implements the protocol currently used to search information repositories on the Jabber network. To date, the jabber:iq:search protocol + * has been used mainly to search for people who have registered with user directories (e.g., the "Jabber User Directory" hosted at users.jabber.org). + * However, the jabber:iq:search protocol is not limited to user directories, and could be used to search other Jabber information repositories + * (such as chatroom directories) or even to provide a Jabber interface to conventional search engines. + * <p/> + * The basic functionality is to query an information repository regarding the possible search fields, to send a search query, and to receive search results. + * + * @author Derek DeMoro + */ +public class UserSearch extends IQ { + + /** + * Creates a new instance of UserSearch. + */ + public UserSearch() { + } + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + buf.append("<query xmlns=\"jabber:iq:search\">"); + buf.append(getExtensionsXML()); + buf.append("</query>"); + return buf.toString(); + } + + /** + * Returns the form for all search fields supported by the search service. + * + * @param con the current Connection. + * @param searchService the search service to use. (ex. search.jivesoftware.com) + * @return the search form received by the server. + * @throws org.jivesoftware.smack.XMPPException + * thrown if a server error has occurred. + */ + public Form getSearchForm(Connection con, String searchService) throws XMPPException { + UserSearch search = new UserSearch(); + search.setType(IQ.Type.GET); + search.setTo(searchService); + + PacketCollector collector = con.createPacketCollector(new PacketIDFilter(search.getPacketID())); + con.sendPacket(search); + + IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Cancel the collector. + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from server on status set."); + } + if (response.getError() != null) { + throw new XMPPException(response.getError()); + } + return Form.getFormFrom(response); + } + + /** + * Sends the filled out answer form to be sent and queried by the search service. + * + * @param con the current Connection. + * @param searchForm the <code>Form</code> to send for querying. + * @param searchService the search service to use. (ex. search.jivesoftware.com) + * @return ReportedData the data found from the query. + * @throws org.jivesoftware.smack.XMPPException + * thrown if a server error has occurred. + */ + public ReportedData sendSearchForm(Connection con, Form searchForm, String searchService) throws XMPPException { + UserSearch search = new UserSearch(); + search.setType(IQ.Type.SET); + search.setTo(searchService); + search.addExtension(searchForm.getDataFormToSend()); + + PacketCollector collector = con.createPacketCollector(new PacketIDFilter(search.getPacketID())); + + con.sendPacket(search); + + IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Cancel the collector. + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from server on status set."); + } + if (response.getError() != null) { + return sendSimpleSearchForm(con, searchForm, searchService); + } + + + return ReportedData.getReportedDataFrom(response); + } + + /** + * Sends the filled out answer form to be sent and queried by the search service. + * + * @param con the current Connection. + * @param searchForm the <code>Form</code> to send for querying. + * @param searchService the search service to use. (ex. search.jivesoftware.com) + * @return ReportedData the data found from the query. + * @throws org.jivesoftware.smack.XMPPException + * thrown if a server error has occurred. + */ + public ReportedData sendSimpleSearchForm(Connection con, Form searchForm, String searchService) throws XMPPException { + SimpleUserSearch search = new SimpleUserSearch(); + search.setForm(searchForm); + search.setType(IQ.Type.SET); + search.setTo(searchService); + + PacketCollector collector = con.createPacketCollector(new PacketIDFilter(search.getPacketID())); + + con.sendPacket(search); + + IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + + // Cancel the collector. + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from server on status set."); + } + if (response.getError() != null) { + throw new XMPPException(response.getError()); + } + + if (response instanceof SimpleUserSearch) { + return ((SimpleUserSearch) response).getReportedData(); + } + return null; + } + + /** + * Internal Search service Provider. + */ + public static class Provider implements IQProvider { + + /** + * Provider Constructor. + */ + public Provider() { + super(); + } + + public IQ parseIQ(XmlPullParser parser) throws Exception { + UserSearch search = null; + SimpleUserSearch simpleUserSearch = new SimpleUserSearch(); + + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG && parser.getName().equals("instructions")) { + buildDataForm(simpleUserSearch, parser.nextText(), parser); + return simpleUserSearch; + } + else if (eventType == XmlPullParser.START_TAG && parser.getName().equals("item")) { + simpleUserSearch.parseItems(parser); + return simpleUserSearch; + } + else if (eventType == XmlPullParser.START_TAG && parser.getNamespace().equals("jabber:x:data")) { + // Otherwise, it must be a packet extension. + search = new UserSearch(); + search.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(), + parser.getNamespace(), parser)); + + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + + if (search != null) { + return search; + } + return simpleUserSearch; + } + } + + private static void buildDataForm(SimpleUserSearch search, String instructions, XmlPullParser parser) throws Exception { + DataForm dataForm = new DataForm(Form.TYPE_FORM); + boolean done = false; + dataForm.setTitle("User Search"); + dataForm.addInstruction(instructions); + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG && !parser.getNamespace().equals("jabber:x:data")) { + String name = parser.getName(); + FormField field = new FormField(name); + + // Handle hard coded values. + if(name.equals("first")){ + field.setLabel("First Name"); + } + else if(name.equals("last")){ + field.setLabel("Last Name"); + } + else if(name.equals("email")){ + field.setLabel("Email Address"); + } + else if(name.equals("nick")){ + field.setLabel("Nickname"); + } + + field.setType(FormField.TYPE_TEXT_SINGLE); + dataForm.addField(field); + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + else if (eventType == XmlPullParser.START_TAG && parser.getNamespace().equals("jabber:x:data")) { + search.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(), + parser.getNamespace(), parser)); + done = true; + } + } + if (search.getExtension("x", "jabber:x:data") == null) { + search.addExtension(dataForm); + } + } + + +} diff --git a/src/org/jivesoftware/smackx/search/UserSearchManager.java b/src/org/jivesoftware/smackx/search/UserSearchManager.java new file mode 100644 index 0000000..858c2a7 --- /dev/null +++ b/src/org/jivesoftware/smackx/search/UserSearchManager.java @@ -0,0 +1,124 @@ +/** + * 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.smackx.search; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smackx.Form; +import org.jivesoftware.smackx.ReportedData; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.jivesoftware.smackx.packet.DiscoverItems; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +/** + * The UserSearchManager is a facade built upon Jabber Search Services (JEP-055) to allow for searching + * repositories on a Jabber Server. This implementation allows for transparency of implementation of + * searching (DataForms or No DataForms), but allows the user to simply use the DataForm model for both + * types of support. + * <pre> + * Connection con = new XMPPConnection("jabber.org"); + * con.login("john", "doe"); + * UserSearchManager search = new UserSearchManager(con, "users.jabber.org"); + * Form searchForm = search.getSearchForm(); + * Form answerForm = searchForm.createAnswerForm(); + * answerForm.setAnswer("last", "DeMoro"); + * ReportedData data = search.getSearchResults(answerForm); + * // Use Returned Data + * </pre> + * + * @author Derek DeMoro + */ +public class UserSearchManager { + + private Connection con; + private UserSearch userSearch; + + /** + * Creates a new UserSearchManager. + * + * @param con the Connection to use. + */ + public UserSearchManager(Connection con) { + this.con = con; + userSearch = new UserSearch(); + } + + /** + * Returns the form to fill out to perform a search. + * + * @param searchService the search service to query. + * @return the form to fill out to perform a search. + * @throws XMPPException thrown if a server error has occurred. + */ + public Form getSearchForm(String searchService) throws XMPPException { + return userSearch.getSearchForm(con, searchService); + } + + /** + * Submits a search form to the server and returns the resulting information + * in the form of <code>ReportedData</code> + * + * @param searchForm the <code>Form</code> to submit for searching. + * @param searchService the name of the search service to use. + * @return the ReportedData returned by the server. + * @throws XMPPException thrown if a server error has occurred. + */ + public ReportedData getSearchResults(Form searchForm, String searchService) throws XMPPException { + return userSearch.sendSearchForm(con, searchForm, searchService); + } + + + /** + * Returns a collection of search services found on the server. + * + * @return a Collection of search services found on the server. + * @throws XMPPException thrown if a server error has occurred. + */ + public Collection<String> getSearchServices() throws XMPPException { + final List<String> searchServices = new ArrayList<String>(); + ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(con); + DiscoverItems items = discoManager.discoverItems(con.getServiceName()); + Iterator<DiscoverItems.Item> iter = items.getItems(); + while (iter.hasNext()) { + DiscoverItems.Item item = iter.next(); + try { + DiscoverInfo info; + try { + info = discoManager.discoverInfo(item.getEntityID()); + } + catch (XMPPException e) { + // Ignore Case + continue; + } + + if (info.containsFeature("jabber:iq:search")) { + searchServices.add(item.getEntityID()); + } + } + catch (Exception e) { + // No info found. + break; + } + } + return searchServices; + } +} diff --git a/src/org/jivesoftware/smackx/workgroup/MetaData.java b/src/org/jivesoftware/smackx/workgroup/MetaData.java new file mode 100644 index 0000000..115a79c --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/MetaData.java @@ -0,0 +1,68 @@ +/**
+ * $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.smackx.workgroup;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smackx.workgroup.util.MetaDataUtils;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * MetaData packet extension.
+ */
+public class MetaData implements PacketExtension {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "metadata";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+ private Map<String, List<String>> metaData;
+
+ public MetaData(Map<String, List<String>> metaData) {
+ this.metaData = metaData;
+ }
+
+ /**
+ * @return the Map of metadata contained by this instance
+ */
+ public Map<String, List<String>> getMetaData() {
+ return metaData;
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String toXML() {
+ return MetaDataUtils.serializeMetaData(this.getMetaData());
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/QueueUser.java b/src/org/jivesoftware/smackx/workgroup/QueueUser.java new file mode 100644 index 0000000..89a1899 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/QueueUser.java @@ -0,0 +1,85 @@ +/**
+ * $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.smackx.workgroup;
+
+import java.util.Date;
+
+/**
+ * An immutable class which wraps up customer-in-queue data return from the server; depending on
+ * the type of information dispatched from the server, not all information will be available in
+ * any given instance.
+ *
+ * @author loki der quaeler
+ */
+public class QueueUser {
+
+ private String userID;
+
+ private int queuePosition;
+ private int estimatedTime;
+ private Date joinDate;
+
+ /**
+ * @param uid the user jid of the customer in the queue
+ * @param position the position customer sits in the queue
+ * @param time the estimate of how much longer the customer will be in the queue in seconds
+ * @param joinedAt the timestamp of when the customer entered the queue
+ */
+ public QueueUser (String uid, int position, int time, Date joinedAt) {
+ super();
+
+ this.userID = uid;
+ this.queuePosition = position;
+ this.estimatedTime = time;
+ this.joinDate = joinedAt;
+ }
+
+ /**
+ * @return the user jid of the customer in the queue
+ */
+ public String getUserID () {
+ return this.userID;
+ }
+
+ /**
+ * @return the position in the queue at which the customer sits, or -1 if the update which
+ * this instance embodies is only a time update instead
+ */
+ public int getQueuePosition () {
+ return this.queuePosition;
+ }
+
+ /**
+ * @return the estimated time remaining of the customer in the queue in seconds, or -1 if
+ * if the update which this instance embodies is only a position update instead
+ */
+ public int getEstimatedRemainingTime () {
+ return this.estimatedTime;
+ }
+
+ /**
+ * @return the timestamp of when this customer entered the queue, or null if the server did not
+ * provide this information
+ */
+ public Date getQueueJoinTimestamp () {
+ return this.joinDate;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/WorkgroupInvitation.java b/src/org/jivesoftware/smackx/workgroup/WorkgroupInvitation.java new file mode 100644 index 0000000..ac3b5b6 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/WorkgroupInvitation.java @@ -0,0 +1,134 @@ +/**
+ * $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.smackx.workgroup;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An immutable class wrapping up the basic information which comprises a group chat invitation.
+ *
+ * @author loki der quaeler
+ */
+public class WorkgroupInvitation {
+
+ protected String uniqueID;
+
+ protected String sessionID;
+
+ protected String groupChatName;
+ protected String issuingWorkgroupName;
+ protected String messageBody;
+ protected String invitationSender;
+ protected Map<String, List<String>> metaData;
+
+ /**
+ * This calls the 5-argument constructor with a null MetaData argument value
+ *
+ * @param jid the jid string with which the issuing AgentSession or Workgroup instance
+ * was created
+ * @param group the jid of the room to which the person is invited
+ * @param workgroup the jid of the workgroup issuing the invitation
+ * @param sessID the session id associated with the pending chat
+ * @param msgBody the body of the message which contained the invitation
+ * @param from the user jid who issued the invitation, if known, null otherwise
+ */
+ public WorkgroupInvitation (String jid, String group, String workgroup,
+ String sessID, String msgBody, String from) {
+ this(jid, group, workgroup, sessID, msgBody, from, null);
+ }
+
+ /**
+ * @param jid the jid string with which the issuing AgentSession or Workgroup instance
+ * was created
+ * @param group the jid of the room to which the person is invited
+ * @param workgroup the jid of the workgroup issuing the invitation
+ * @param sessID the session id associated with the pending chat
+ * @param msgBody the body of the message which contained the invitation
+ * @param from the user jid who issued the invitation, if known, null otherwise
+ * @param metaData the metadata sent with the invitation
+ */
+ public WorkgroupInvitation (String jid, String group, String workgroup, String sessID, String msgBody,
+ String from, Map<String, List<String>> metaData) {
+ super();
+
+ this.uniqueID = jid;
+ this.sessionID = sessID;
+ this.groupChatName = group;
+ this.issuingWorkgroupName = workgroup;
+ this.messageBody = msgBody;
+ this.invitationSender = from;
+ this.metaData = metaData;
+ }
+
+ /**
+ * @return the jid string with which the issuing AgentSession or Workgroup instance
+ * was created.
+ */
+ public String getUniqueID () {
+ return this.uniqueID;
+ }
+
+ /**
+ * @return the session id associated with the pending chat; working backwards temporally,
+ * this session id should match the session id to the corresponding offer request
+ * which resulted in this invitation.
+ */
+ public String getSessionID () {
+ return this.sessionID;
+ }
+
+ /**
+ * @return the jid of the room to which the person is invited.
+ */
+ public String getGroupChatName () {
+ return this.groupChatName;
+ }
+
+ /**
+ * @return the name of the workgroup from which the invitation was issued.
+ */
+ public String getWorkgroupName () {
+ return this.issuingWorkgroupName;
+ }
+
+ /**
+ * @return the contents of the body-block of the message that housed this invitation.
+ */
+ public String getMessageBody () {
+ return this.messageBody;
+ }
+
+ /**
+ * @return the user who issued the invitation, or null if it wasn't known.
+ */
+ public String getInvitationSender () {
+ return this.invitationSender;
+ }
+
+ /**
+ * @return the meta data associated with the invitation, or null if this instance was
+ * constructed with none
+ */
+ public Map<String, List<String>> getMetaData () {
+ return this.metaData;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/WorkgroupInvitationListener.java b/src/org/jivesoftware/smackx/workgroup/WorkgroupInvitationListener.java new file mode 100644 index 0000000..bc73242 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/WorkgroupInvitationListener.java @@ -0,0 +1,39 @@ +/**
+ * $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.smackx.workgroup;
+
+/**
+ * An interface which all classes interested in hearing about group chat invitations should
+ * implement.
+ *
+ * @author loki der quaeler
+ */
+public interface WorkgroupInvitationListener {
+
+ /**
+ * The implementing class instance will be notified via this method when an invitation
+ * to join a group chat has been received from the server.
+ *
+ * @param invitation an Invitation instance embodying the information pertaining to the
+ * invitation
+ */
+ public void invitationReceived(WorkgroupInvitation invitation);
+
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/Agent.java b/src/org/jivesoftware/smackx/workgroup/agent/Agent.java new file mode 100644 index 0000000..bebac37 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/Agent.java @@ -0,0 +1,138 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+import org.jivesoftware.smackx.workgroup.packet.AgentInfo;
+import org.jivesoftware.smackx.workgroup.packet.AgentWorkgroups;
+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.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+
+import java.util.Collection;
+
+/**
+ * The <code>Agent</code> class is used to represent one agent in a Workgroup Queue.
+ *
+ * @author Derek DeMoro
+ */
+public class Agent {
+ private Connection connection;
+ private String workgroupJID;
+
+ public static Collection<String> getWorkgroups(String serviceJID, String agentJID, Connection connection) throws XMPPException {
+ AgentWorkgroups request = new AgentWorkgroups(agentJID);
+ request.setTo(serviceJID);
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ // Send the request
+ connection.sendPacket(request);
+
+ AgentWorkgroups response = (AgentWorkgroups)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response.getWorkgroups();
+ }
+
+ /**
+ * Constructs an Agent.
+ */
+ Agent(Connection connection, String workgroupJID) {
+ this.connection = connection;
+ this.workgroupJID = workgroupJID;
+ }
+
+ /**
+ * Return the agents JID
+ *
+ * @return - the agents JID.
+ */
+ public String getUser() {
+ return connection.getUser();
+ }
+
+ /**
+ * Return the agents name.
+ *
+ * @return - the agents name.
+ */
+ public String getName() throws XMPPException {
+ AgentInfo agentInfo = new AgentInfo();
+ agentInfo.setType(IQ.Type.GET);
+ agentInfo.setTo(workgroupJID);
+ agentInfo.setFrom(getUser());
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(agentInfo.getPacketID()));
+ // Send the request
+ connection.sendPacket(agentInfo);
+
+ AgentInfo response = (AgentInfo)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response.getName();
+ }
+
+ /**
+ * Changes the name of the agent in the server. The server may have this functionality
+ * disabled for all the agents or for this agent in particular. If the agent is not
+ * allowed to change his name then an exception will be thrown with a service_unavailable
+ * error code.
+ *
+ * @param newName the new name of the agent.
+ * @throws XMPPException if the agent is not allowed to change his name or no response was
+ * obtained from the server.
+ */
+ public void setName(String newName) throws XMPPException {
+ AgentInfo agentInfo = new AgentInfo();
+ agentInfo.setType(IQ.Type.SET);
+ agentInfo.setTo(workgroupJID);
+ agentInfo.setFrom(getUser());
+ agentInfo.setName(newName);
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(agentInfo.getPacketID()));
+ // Send the request
+ connection.sendPacket(agentInfo);
+
+ IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return;
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/AgentRoster.java b/src/org/jivesoftware/smackx/workgroup/agent/AgentRoster.java new file mode 100644 index 0000000..70c95ee --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/AgentRoster.java @@ -0,0 +1,386 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+import org.jivesoftware.smackx.workgroup.packet.AgentStatus;
+import org.jivesoftware.smackx.workgroup.packet.AgentStatusRequest;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Manges information about the agents in a workgroup and their presence.
+ *
+ * @author Matt Tucker
+ * @see AgentSession#getAgentRoster()
+ */
+public class AgentRoster {
+
+ private static final int EVENT_AGENT_ADDED = 0;
+ private static final int EVENT_AGENT_REMOVED = 1;
+ private static final int EVENT_PRESENCE_CHANGED = 2;
+
+ private Connection connection;
+ private String workgroupJID;
+ private List<String> entries;
+ private List<AgentRosterListener> listeners;
+ private Map<String, Map<String, Presence>> presenceMap;
+ // The roster is marked as initialized when at least a single roster packet
+ // has been recieved and processed.
+ boolean rosterInitialized = false;
+
+ /**
+ * Constructs a new AgentRoster.
+ *
+ * @param connection an XMPP connection.
+ */
+ AgentRoster(Connection connection, String workgroupJID) {
+ this.connection = connection;
+ this.workgroupJID = workgroupJID;
+ entries = new ArrayList<String>();
+ listeners = new ArrayList<AgentRosterListener>();
+ presenceMap = new HashMap<String, Map<String, Presence>>();
+ // Listen for any roster packets.
+ PacketFilter rosterFilter = new PacketTypeFilter(AgentStatusRequest.class);
+ connection.addPacketListener(new AgentStatusListener(), rosterFilter);
+ // Listen for any presence packets.
+ connection.addPacketListener(new PresencePacketListener(),
+ new PacketTypeFilter(Presence.class));
+
+ // Send request for roster.
+ AgentStatusRequest request = new AgentStatusRequest();
+ request.setTo(workgroupJID);
+ connection.sendPacket(request);
+ }
+
+ /**
+ * Reloads the entire roster from the server. This is an asynchronous operation,
+ * which means the method will return immediately, and the roster will be
+ * reloaded at a later point when the server responds to the reload request.
+ */
+ public void reload() {
+ AgentStatusRequest request = new AgentStatusRequest();
+ request.setTo(workgroupJID);
+ connection.sendPacket(request);
+ }
+
+ /**
+ * 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 listener an agent roster listener.
+ */
+ public void addListener(AgentRosterListener listener) {
+ synchronized (listeners) {
+ if (!listeners.contains(listener)) {
+ listeners.add(listener);
+
+ // Fire events for the existing entries and presences in the roster
+ for (Iterator<String> it = getAgents().iterator(); it.hasNext();) {
+ String jid = it.next();
+ // Check again in case the agent is no longer in the roster (highly unlikely
+ // but possible)
+ if (entries.contains(jid)) {
+ // Fire the agent added event
+ listener.agentAdded(jid);
+ Map<String,Presence> userPresences = presenceMap.get(jid);
+ if (userPresences != null) {
+ Iterator<Presence> presences = userPresences.values().iterator();
+ while (presences.hasNext()) {
+ // Fire the presence changed event
+ listener.presenceChanged(presences.next());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 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 listener a roster listener.
+ */
+ public void removeListener(AgentRosterListener listener) {
+ synchronized (listeners) {
+ listeners.remove(listener);
+ }
+ }
+
+ /**
+ * Returns a count of all agents in the workgroup.
+ *
+ * @return the number of agents in the workgroup.
+ */
+ public int getAgentCount() {
+ return entries.size();
+ }
+
+ /**
+ * Returns all agents (String JID values) in the workgroup.
+ *
+ * @return all entries in the roster.
+ */
+ public Set<String> getAgents() {
+ Set<String> agents = new HashSet<String>();
+ synchronized (entries) {
+ for (Iterator<String> i = entries.iterator(); i.hasNext();) {
+ agents.add(i.next());
+ }
+ }
+ return Collections.unmodifiableSet(agents);
+ }
+
+ /**
+ * Returns true if the specified XMPP address is an agent in the workgroup.
+ *
+ * @param jid the XMPP address of the agent (eg "jsmith@example.com"). The
+ * address can be in any valid format (e.g. "domain/resource", "user@domain"
+ * or "user@domain/resource").
+ * @return true if the XMPP address is an agent in the workgroup.
+ */
+ public boolean contains(String jid) {
+ if (jid == null) {
+ return false;
+ }
+ synchronized (entries) {
+ for (Iterator<String> i = entries.iterator(); i.hasNext();) {
+ String entry = i.next();
+ if (entry.toLowerCase().equals(jid.toLowerCase())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the presence info for a particular agent, or <tt>null</tt> if the agent
+ * is unavailable (offline) or if no presence information is available.<p>
+ *
+ * @param user a fully qualified xmpp JID. The address could be in any valid format (e.g.
+ * "domain/resource", "user@domain" or "user@domain/resource").
+ * @return the agent's current presence, or <tt>null</tt> if the agent is unavailable
+ * or if no presence information is available..
+ */
+ public Presence getPresence(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 presence;
+ }
+ else {
+ // Find the resource with the highest priority
+ // Might be changed to use the resource with the highest availability instead.
+ Iterator<String> it = userPresences.keySet().iterator();
+ Presence p;
+ Presence presence = null;
+
+ while (it.hasNext()) {
+ p = (Presence)userPresences.get(it.next());
+ if (presence == null){
+ presence = p;
+ }
+ else {
+ if (p.getPriority() > presence.getPriority()) {
+ presence = p;
+ }
+ }
+ }
+ if (presence == null) {
+ presence = new Presence(Presence.Type.unavailable);
+ presence.setFrom(user);
+ return presence;
+ }
+ else {
+ return presence;
+ }
+ }
+ }
+
+ /**
+ * Returns the key to use in the presenceMap for a fully qualified xmpp ID. The roster
+ * can contain any valid address format such us "domain/resource", "user@domain" or
+ * "user@domain/resource". If the roster contains an entry associated with the fully qualified
+ * xmpp ID then use the fully qualified xmpp ID as the key in presenceMap, otherwise use the
+ * bare address. Note: When the key in presenceMap is a fully qualified xmpp ID, the
+ * userPresences is useless since it will always contain one entry for the user.
+ *
+ * @param user the fully qualified xmpp ID, e.g. jdoe@example.com/Work.
+ * @return the key to use in the presenceMap for the fully qualified xmpp ID.
+ */
+ private String getPresenceMapKey(String user) {
+ String key = user;
+ if (!contains(user)) {
+ key = StringUtils.parseBareAddress(user).toLowerCase();
+ }
+ return key;
+ }
+
+ /**
+ * Fires event to listeners.
+ */
+ private void fireEvent(int eventType, Object eventObject) {
+ AgentRosterListener[] listeners = null;
+ synchronized (this.listeners) {
+ listeners = new AgentRosterListener[this.listeners.size()];
+ this.listeners.toArray(listeners);
+ }
+ for (int i = 0; i < listeners.length; i++) {
+ switch (eventType) {
+ case EVENT_AGENT_ADDED:
+ listeners[i].agentAdded((String)eventObject);
+ break;
+ case EVENT_AGENT_REMOVED:
+ listeners[i].agentRemoved((String)eventObject);
+ break;
+ case EVENT_PRESENCE_CHANGED:
+ listeners[i].presenceChanged((Presence)eventObject);
+ break;
+ }
+ }
+ }
+
+ /**
+ * 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();
+ if (from == null) {
+ // TODO Check if we need to ignore these presences or this is a server bug?
+ System.out.println("Presence with no FROM: " + presence.toXML());
+ return;
+ }
+ String key = getPresenceMapKey(from);
+
+ // If an "available" packet, add it to the presence map. Each presence map will hold
+ // for a particular user a map with the presence packets saved for each resource.
+ if (presence.getType() == Presence.Type.available) {
+ // Ignore the presence packet unless it has an agent status extension.
+ AgentStatus agentStatus = (AgentStatus)presence.getExtension(
+ AgentStatus.ELEMENT_NAME, AgentStatus.NAMESPACE);
+ if (agentStatus == null) {
+ return;
+ }
+ // Ensure that this presence is coming from an Agent of the same workgroup
+ // of this Agent
+ else if (!workgroupJID.equals(agentStatus.getWorkgroupJID())) {
+ return;
+ }
+ Map<String, Presence> userPresences;
+ // Get the user presence map
+ if (presenceMap.get(key) == null) {
+ userPresences = new HashMap<String, Presence>();
+ presenceMap.put(key, userPresences);
+ }
+ else {
+ userPresences = presenceMap.get(key);
+ }
+ // Add the new presence, using the resources as a key.
+ synchronized (userPresences) {
+ userPresences.put(StringUtils.parseResource(from), presence);
+ }
+ // Fire an event.
+ synchronized (entries) {
+ for (Iterator<String> i = entries.iterator(); i.hasNext();) {
+ String entry = i.next();
+ if (entry.toLowerCase().equals(StringUtils.parseBareAddress(key).toLowerCase())) {
+ fireEvent(EVENT_PRESENCE_CHANGED, packet);
+ }
+ }
+ }
+ }
+ // If an "unavailable" packet, remove any entries in the presence map.
+ else if (presence.getType() == Presence.Type.unavailable) {
+ if (presenceMap.get(key) != null) {
+ Map<String,Presence> userPresences = presenceMap.get(key);
+ synchronized (userPresences) {
+ userPresences.remove(StringUtils.parseResource(from));
+ }
+ if (userPresences.isEmpty()) {
+ presenceMap.remove(key);
+ }
+ }
+ // Fire an event.
+ synchronized (entries) {
+ for (Iterator<String> i = entries.iterator(); i.hasNext();) {
+ String entry = (String)i.next();
+ if (entry.toLowerCase().equals(StringUtils.parseBareAddress(key).toLowerCase())) {
+ fireEvent(EVENT_PRESENCE_CHANGED, packet);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Listens for all roster packets and processes them.
+ */
+ private class AgentStatusListener implements PacketListener {
+
+ public void processPacket(Packet packet) {
+ if (packet instanceof AgentStatusRequest) {
+ AgentStatusRequest statusRequest = (AgentStatusRequest)packet;
+ for (Iterator<AgentStatusRequest.Item> i = statusRequest.getAgents().iterator(); i.hasNext();) {
+ AgentStatusRequest.Item item = i.next();
+ String agentJID = item.getJID();
+ if ("remove".equals(item.getType())) {
+
+ // Removing the user from the roster, so remove any presence information
+ // about them.
+ String key = StringUtils.parseName(StringUtils.parseName(agentJID) + "@" +
+ StringUtils.parseServer(agentJID));
+ presenceMap.remove(key);
+ // Fire event for roster listeners.
+ fireEvent(EVENT_AGENT_REMOVED, agentJID);
+ }
+ else {
+ entries.add(agentJID);
+ // Fire event for roster listeners.
+ fireEvent(EVENT_AGENT_ADDED, agentJID);
+ }
+ }
+
+ // Mark the roster as initialized.
+ rosterInitialized = true;
+ }
+ }
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/agent/AgentRosterListener.java b/src/org/jivesoftware/smackx/workgroup/agent/AgentRosterListener.java new file mode 100644 index 0000000..4db9203 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/AgentRosterListener.java @@ -0,0 +1,35 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+import org.jivesoftware.smack.packet.Presence;
+
+/**
+ *
+ * @author Matt Tucker
+ */
+public interface AgentRosterListener {
+
+ public void agentAdded(String jid);
+
+ public void agentRemoved(String jid);
+
+ public void presenceChanged(Presence presence);
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/AgentSession.java b/src/org/jivesoftware/smackx/workgroup/agent/AgentSession.java new file mode 100644 index 0000000..46d19d0 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/AgentSession.java @@ -0,0 +1,1185 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+import org.jivesoftware.smackx.workgroup.MetaData;
+import org.jivesoftware.smackx.workgroup.QueueUser;
+import org.jivesoftware.smackx.workgroup.WorkgroupInvitation;
+import org.jivesoftware.smackx.workgroup.WorkgroupInvitationListener;
+import org.jivesoftware.smackx.workgroup.ext.history.AgentChatHistory;
+import org.jivesoftware.smackx.workgroup.ext.history.ChatMetadata;
+import org.jivesoftware.smackx.workgroup.ext.macros.MacroGroup;
+import org.jivesoftware.smackx.workgroup.ext.macros.Macros;
+import org.jivesoftware.smackx.workgroup.ext.notes.ChatNotes;
+import org.jivesoftware.smackx.workgroup.packet.*;
+import org.jivesoftware.smackx.workgroup.settings.GenericSettings;
+import org.jivesoftware.smackx.workgroup.settings.SearchSettings;
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.ReportedData;
+import org.jivesoftware.smackx.packet.MUCUser;
+
+import java.util.*;
+
+/**
+ * This class embodies the agent's active presence within a given workgroup. The application
+ * should have N instances of this class, where N is the number of workgroups to which the
+ * owning agent of the application belongs. This class provides all functionality that a
+ * session within a given workgroup is expected to have from an agent's perspective -- setting
+ * the status, tracking the status of queues to which the agent belongs within the workgroup, and
+ * dequeuing customers.
+ *
+ * @author Matt Tucker
+ * @author Derek DeMoro
+ */
+public class AgentSession {
+
+ private Connection connection;
+
+ private String workgroupJID;
+
+ private boolean online = false;
+ private Presence.Mode presenceMode;
+ private int maxChats;
+ private final Map<String, List<String>> metaData;
+
+ private Map<String, WorkgroupQueue> queues;
+
+ private final List<OfferListener> offerListeners;
+ private final List<WorkgroupInvitationListener> invitationListeners;
+ private final List<QueueUsersListener> queueUsersListeners;
+
+ private AgentRoster agentRoster = null;
+ private TranscriptManager transcriptManager;
+ private TranscriptSearchManager transcriptSearchManager;
+ private Agent agent;
+ private PacketListener packetListener;
+
+ /**
+ * Constructs a new agent session instance. Note, the {@link #setOnline(boolean)}
+ * method must be called with an argument of <tt>true</tt> to mark the agent
+ * as available to accept chat requests.
+ *
+ * @param connection a connection instance which must have already gone through
+ * authentication.
+ * @param workgroupJID the fully qualified JID of the workgroup.
+ */
+ public AgentSession(String workgroupJID, Connection connection) {
+ // Login must have been done before passing in connection.
+ if (!connection.isAuthenticated()) {
+ throw new IllegalStateException("Must login to server before creating workgroup.");
+ }
+
+ this.workgroupJID = workgroupJID;
+ this.connection = connection;
+ this.transcriptManager = new TranscriptManager(connection);
+ this.transcriptSearchManager = new TranscriptSearchManager(connection);
+
+ this.maxChats = -1;
+
+ this.metaData = new HashMap<String, List<String>>();
+
+ this.queues = new HashMap<String, WorkgroupQueue>();
+
+ offerListeners = new ArrayList<OfferListener>();
+ invitationListeners = new ArrayList<WorkgroupInvitationListener>();
+ queueUsersListeners = new ArrayList<QueueUsersListener>();
+
+ // Create a filter to listen for packets we're interested in.
+ OrFilter filter = new OrFilter();
+ filter.addFilter(new PacketTypeFilter(OfferRequestProvider.OfferRequestPacket.class));
+ filter.addFilter(new PacketTypeFilter(OfferRevokeProvider.OfferRevokePacket.class));
+ filter.addFilter(new PacketTypeFilter(Presence.class));
+ filter.addFilter(new PacketTypeFilter(Message.class));
+
+ packetListener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ try {
+ handlePacket(packet);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ };
+ connection.addPacketListener(packetListener, filter);
+ // Create the agent associated to this session
+ agent = new Agent(connection, workgroupJID);
+ }
+
+ /**
+ * Close the agent session. The underlying connection will remain opened but the
+ * packet listeners that were added by this agent session will be removed.
+ */
+ public void close() {
+ connection.removePacketListener(packetListener);
+ }
+
+ /**
+ * Returns the agent roster for the workgroup, which contains
+ *
+ * @return the AgentRoster
+ */
+ public AgentRoster getAgentRoster() {
+ if (agentRoster == null) {
+ agentRoster = new AgentRoster(connection, workgroupJID);
+ }
+
+ // This might be the first time the user has asked for the roster. If so, we
+ // want to wait up to 2 seconds for the server to send back the list of agents.
+ // This behavior shields API users from having to worry about the fact that the
+ // operation is asynchronous, although they'll still have to listen for changes
+ // to the roster.
+ int elapsed = 0;
+ while (!agentRoster.rosterInitialized && elapsed <= 2000) {
+ try {
+ Thread.sleep(500);
+ }
+ catch (Exception e) {
+ // Ignore
+ }
+ elapsed += 500;
+ }
+ return agentRoster;
+ }
+
+ /**
+ * Returns the agent's current presence mode.
+ *
+ * @return the agent's current presence mode.
+ */
+ public Presence.Mode getPresenceMode() {
+ return presenceMode;
+ }
+
+ /**
+ * Returns the maximum number of chats the agent can participate in.
+ *
+ * @return the maximum number of chats the agent can participate in.
+ */
+ public int getMaxChats() {
+ return maxChats;
+ }
+
+ /**
+ * Returns true if the agent is online with the workgroup.
+ *
+ * @return true if the agent is online with the workgroup.
+ */
+ public boolean isOnline() {
+ return online;
+ }
+
+ /**
+ * Allows the addition of a new key-value pair to the agent's meta data, if the value is
+ * new data, the revised meta data will be rebroadcast in an agent's presence broadcast.
+ *
+ * @param key the meta data key
+ * @param val the non-null meta data value
+ * @throws XMPPException if an exception occurs.
+ */
+ public void setMetaData(String key, String val) throws XMPPException {
+ synchronized (this.metaData) {
+ List<String> oldVals = metaData.get(key);
+
+ if ((oldVals == null) || (!oldVals.get(0).equals(val))) {
+ oldVals.set(0, val);
+
+ setStatus(presenceMode, maxChats);
+ }
+ }
+ }
+
+ /**
+ * Allows the removal of data from the agent's meta data, if the key represents existing data,
+ * the revised meta data will be rebroadcast in an agent's presence broadcast.
+ *
+ * @param key the meta data key.
+ * @throws XMPPException if an exception occurs.
+ */
+ public void removeMetaData(String key) throws XMPPException {
+ synchronized (this.metaData) {
+ List<String> oldVal = metaData.remove(key);
+
+ if (oldVal != null) {
+ setStatus(presenceMode, maxChats);
+ }
+ }
+ }
+
+ /**
+ * Allows the retrieval of meta data for a specified key.
+ *
+ * @param key the meta data key
+ * @return the meta data value associated with the key or <tt>null</tt> if the meta-data
+ * doesn't exist..
+ */
+ public List<String> getMetaData(String key) {
+ return metaData.get(key);
+ }
+
+ /**
+ * Sets whether the agent is online with the workgroup. If the user tries to go online with
+ * the workgroup but is not allowed to be an agent, an XMPPError with error code 401 will
+ * be thrown.
+ *
+ * @param online true to set the agent as online with the workgroup.
+ * @throws XMPPException if an error occurs setting the online status.
+ */
+ public void setOnline(boolean online) throws XMPPException {
+ // If the online status hasn't changed, do nothing.
+ if (this.online == online) {
+ return;
+ }
+
+ Presence presence;
+
+ // If the user is going online...
+ if (online) {
+ presence = new Presence(Presence.Type.available);
+ presence.setTo(workgroupJID);
+ presence.addExtension(new DefaultPacketExtension(AgentStatus.ELEMENT_NAME,
+ AgentStatus.NAMESPACE));
+
+ PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class), new FromContainsFilter(workgroupJID)));
+
+ connection.sendPacket(presence);
+
+ presence = (Presence)collector.nextResult(5000);
+ collector.cancel();
+ if (!presence.isAvailable()) {
+ throw new XMPPException("No response from server on status set.");
+ }
+
+ if (presence.getError() != null) {
+ throw new XMPPException(presence.getError());
+ }
+
+ // We can safely update this iv since we didn't get any error
+ this.online = online;
+ }
+ // Otherwise the user is going offline...
+ else {
+ // Update this iv now since we don't care at this point of any error
+ this.online = online;
+
+ presence = new Presence(Presence.Type.unavailable);
+ presence.setTo(workgroupJID);
+ presence.addExtension(new DefaultPacketExtension(AgentStatus.ELEMENT_NAME,
+ AgentStatus.NAMESPACE));
+ connection.sendPacket(presence);
+ }
+ }
+
+ /**
+ * Sets the agent's current status with the workgroup. The presence mode affects
+ * how offers are routed to the agent. The possible presence modes with their
+ * meanings are as follows:<ul>
+ * <p/>
+ * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats
+ * (equivalent to Presence.Mode.CHAT).
+ * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.
+ * However, special case, or extreme urgency chats may still be offered to the agent.
+ * <li>Presence.Mode.AWAY -- the agent is not available and should not
+ * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>
+ * <p/>
+ * The max chats value is the maximum number of chats the agent is willing to have
+ * routed to them at once. Some servers may be configured to only accept max chat
+ * values in a certain range; for example, between two and five. In that case, the
+ * maxChats value the agent sends may be adjusted by the server to a value within that
+ * range.
+ *
+ * @param presenceMode the presence mode of the agent.
+ * @param maxChats the maximum number of chats the agent is willing to accept.
+ * @throws XMPPException if an error occurs setting the agent status.
+ * @throws IllegalStateException if the agent is not online with the workgroup.
+ */
+ public void setStatus(Presence.Mode presenceMode, int maxChats) throws XMPPException {
+ setStatus(presenceMode, maxChats, null);
+ }
+
+ /**
+ * Sets the agent's current status with the workgroup. The presence mode affects how offers
+ * are routed to the agent. The possible presence modes with their meanings are as follows:<ul>
+ * <p/>
+ * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats
+ * (equivalent to Presence.Mode.CHAT).
+ * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.
+ * However, special case, or extreme urgency chats may still be offered to the agent.
+ * <li>Presence.Mode.AWAY -- the agent is not available and should not
+ * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>
+ * <p/>
+ * The max chats value is the maximum number of chats the agent is willing to have routed to
+ * them at once. Some servers may be configured to only accept max chat values in a certain
+ * range; for example, between two and five. In that case, the maxChats value the agent sends
+ * may be adjusted by the server to a value within that range.
+ *
+ * @param presenceMode the presence mode of the agent.
+ * @param maxChats the maximum number of chats the agent is willing to accept.
+ * @param status sets the status message of the presence update.
+ * @throws XMPPException if an error occurs setting the agent status.
+ * @throws IllegalStateException if the agent is not online with the workgroup.
+ */
+ public void setStatus(Presence.Mode presenceMode, int maxChats, String status)
+ throws XMPPException {
+ if (!online) {
+ throw new IllegalStateException("Cannot set status when the agent is not online.");
+ }
+
+ if (presenceMode == null) {
+ presenceMode = Presence.Mode.available;
+ }
+ this.presenceMode = presenceMode;
+ this.maxChats = maxChats;
+
+ Presence presence = new Presence(Presence.Type.available);
+ presence.setMode(presenceMode);
+ presence.setTo(this.getWorkgroupJID());
+
+ if (status != null) {
+ presence.setStatus(status);
+ }
+ // Send information about max chats and current chats as a packet extension.
+ DefaultPacketExtension agentStatus = new DefaultPacketExtension(AgentStatus.ELEMENT_NAME,
+ AgentStatus.NAMESPACE);
+ agentStatus.setValue("max-chats", "" + maxChats);
+ presence.addExtension(agentStatus);
+ presence.addExtension(new MetaData(this.metaData));
+
+ PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class), new FromContainsFilter(workgroupJID)));
+
+ this.connection.sendPacket(presence);
+
+ presence = (Presence)collector.nextResult(5000);
+ collector.cancel();
+ if (!presence.isAvailable()) {
+ throw new XMPPException("No response from server on status set.");
+ }
+
+ if (presence.getError() != null) {
+ throw new XMPPException(presence.getError());
+ }
+ }
+
+ /**
+ * Sets the agent's current status with the workgroup. The presence mode affects how offers
+ * are routed to the agent. The possible presence modes with their meanings are as follows:<ul>
+ * <p/>
+ * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats
+ * (equivalent to Presence.Mode.CHAT).
+ * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.
+ * However, special case, or extreme urgency chats may still be offered to the agent.
+ * <li>Presence.Mode.AWAY -- the agent is not available and should not
+ * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>
+ *
+ * @param presenceMode the presence mode of the agent.
+ * @param status sets the status message of the presence update.
+ * @throws XMPPException if an error occurs setting the agent status.
+ * @throws IllegalStateException if the agent is not online with the workgroup.
+ */
+ public void setStatus(Presence.Mode presenceMode, String status) throws XMPPException {
+ if (!online) {
+ throw new IllegalStateException("Cannot set status when the agent is not online.");
+ }
+
+ if (presenceMode == null) {
+ presenceMode = Presence.Mode.available;
+ }
+ this.presenceMode = presenceMode;
+
+ Presence presence = new Presence(Presence.Type.available);
+ presence.setMode(presenceMode);
+ presence.setTo(this.getWorkgroupJID());
+
+ if (status != null) {
+ presence.setStatus(status);
+ }
+ presence.addExtension(new MetaData(this.metaData));
+
+ PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class),
+ new FromContainsFilter(workgroupJID)));
+
+ this.connection.sendPacket(presence);
+
+ presence = (Presence)collector.nextResult(5000);
+ collector.cancel();
+ if (!presence.isAvailable()) {
+ throw new XMPPException("No response from server on status set.");
+ }
+
+ if (presence.getError() != null) {
+ throw new XMPPException(presence.getError());
+ }
+ }
+
+ /**
+ * Removes a user from the workgroup queue. This is an administrative action that the
+ * <p/>
+ * The agent is not guaranteed of having privileges to perform this action; an exception
+ * denying the request may be thrown.
+ *
+ * @param userID the ID of the user to remove.
+ * @throws XMPPException if an exception occurs.
+ */
+ public void dequeueUser(String userID) throws XMPPException {
+ // todo: this method simply won't work right now.
+ DepartQueuePacket departPacket = new DepartQueuePacket(this.workgroupJID);
+
+ // PENDING
+ this.connection.sendPacket(departPacket);
+ }
+
+ /**
+ * Returns the transcripts of a given user. The answer will contain the complete history of
+ * conversations that a user had.
+ *
+ * @param userID the id of the user to get his conversations.
+ * @return the transcripts of a given user.
+ * @throws XMPPException if an error occurs while getting the information.
+ */
+ public Transcripts getTranscripts(String userID) throws XMPPException {
+ return transcriptManager.getTranscripts(workgroupJID, userID);
+ }
+
+ /**
+ * Returns the full conversation transcript of a given session.
+ *
+ * @param sessionID the id of the session to get the full transcript.
+ * @return the full conversation transcript of a given session.
+ * @throws XMPPException if an error occurs while getting the information.
+ */
+ public Transcript getTranscript(String sessionID) throws XMPPException {
+ return transcriptManager.getTranscript(workgroupJID, sessionID);
+ }
+
+ /**
+ * Returns the Form to use for searching transcripts. It is unlikely that the server
+ * will change the form (without a restart) so it is safe to keep the returned form
+ * for future submissions.
+ *
+ * @return the Form to use for searching transcripts.
+ * @throws XMPPException if an error occurs while sending the request to the server.
+ */
+ public Form getTranscriptSearchForm() throws XMPPException {
+ return transcriptSearchManager.getSearchForm(StringUtils.parseServer(workgroupJID));
+ }
+
+ /**
+ * Submits the completed form and returns the result of the transcript search. The result
+ * will include all the data returned from the server so be careful with the amount of
+ * data that the search may return.
+ *
+ * @param completedForm the filled out search form.
+ * @return the result of the transcript search.
+ * @throws XMPPException if an error occurs while submiting the search to the server.
+ */
+ public ReportedData searchTranscripts(Form completedForm) throws XMPPException {
+ return transcriptSearchManager.submitSearch(StringUtils.parseServer(workgroupJID),
+ completedForm);
+ }
+
+ /**
+ * Asks the workgroup for information about the occupants of the specified room. The returned
+ * information will include the real JID of the occupants, the nickname of the user in the
+ * room as well as the date when the user joined the room.
+ *
+ * @param roomID the room to get information about its occupants.
+ * @return information about the occupants of the specified room.
+ * @throws XMPPException if an error occurs while getting information from the server.
+ */
+ public OccupantsInfo getOccupantsInfo(String roomID) throws XMPPException {
+ OccupantsInfo request = new OccupantsInfo(roomID);
+ request.setType(IQ.Type.GET);
+ request.setTo(workgroupJID);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+ OccupantsInfo response = (OccupantsInfo)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+ }
+
+ /**
+ * @return the fully-qualified name of the workgroup for which this session exists
+ */
+ public String getWorkgroupJID() {
+ return workgroupJID;
+ }
+
+ /**
+ * Returns the Agent associated to this session.
+ *
+ * @return the Agent associated to this session.
+ */
+ public Agent getAgent() {
+ return agent;
+ }
+
+ /**
+ * @param queueName the name of the queue
+ * @return an instance of WorkgroupQueue for the argument queue name, or null if none exists
+ */
+ public WorkgroupQueue getQueue(String queueName) {
+ return queues.get(queueName);
+ }
+
+ public Iterator<WorkgroupQueue> getQueues() {
+ return Collections.unmodifiableMap((new HashMap<String, WorkgroupQueue>(queues))).values().iterator();
+ }
+
+ public void addQueueUsersListener(QueueUsersListener listener) {
+ synchronized (queueUsersListeners) {
+ if (!queueUsersListeners.contains(listener)) {
+ queueUsersListeners.add(listener);
+ }
+ }
+ }
+
+ public void removeQueueUsersListener(QueueUsersListener listener) {
+ synchronized (queueUsersListeners) {
+ queueUsersListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Adds an offer listener.
+ *
+ * @param offerListener the offer listener.
+ */
+ public void addOfferListener(OfferListener offerListener) {
+ synchronized (offerListeners) {
+ if (!offerListeners.contains(offerListener)) {
+ offerListeners.add(offerListener);
+ }
+ }
+ }
+
+ /**
+ * Removes an offer listener.
+ *
+ * @param offerListener the offer listener.
+ */
+ public void removeOfferListener(OfferListener offerListener) {
+ synchronized (offerListeners) {
+ offerListeners.remove(offerListener);
+ }
+ }
+
+ /**
+ * Adds an invitation listener.
+ *
+ * @param invitationListener the invitation listener.
+ */
+ public void addInvitationListener(WorkgroupInvitationListener invitationListener) {
+ synchronized (invitationListeners) {
+ if (!invitationListeners.contains(invitationListener)) {
+ invitationListeners.add(invitationListener);
+ }
+ }
+ }
+
+ /**
+ * Removes an invitation listener.
+ *
+ * @param invitationListener the invitation listener.
+ */
+ public void removeInvitationListener(WorkgroupInvitationListener invitationListener) {
+ synchronized (invitationListeners) {
+ invitationListeners.remove(invitationListener);
+ }
+ }
+
+ private void fireOfferRequestEvent(OfferRequestProvider.OfferRequestPacket requestPacket) {
+ Offer offer = new Offer(this.connection, this, requestPacket.getUserID(),
+ requestPacket.getUserJID(), this.getWorkgroupJID(),
+ new Date((new Date()).getTime() + (requestPacket.getTimeout() * 1000)),
+ requestPacket.getSessionID(), requestPacket.getMetaData(), requestPacket.getContent());
+
+ synchronized (offerListeners) {
+ for (OfferListener listener : offerListeners) {
+ listener.offerReceived(offer);
+ }
+ }
+ }
+
+ private void fireOfferRevokeEvent(OfferRevokeProvider.OfferRevokePacket orp) {
+ RevokedOffer revokedOffer = new RevokedOffer(orp.getUserJID(), orp.getUserID(),
+ this.getWorkgroupJID(), orp.getSessionID(), orp.getReason(), new Date());
+
+ synchronized (offerListeners) {
+ for (OfferListener listener : offerListeners) {
+ listener.offerRevoked(revokedOffer);
+ }
+ }
+ }
+
+ private void fireInvitationEvent(String groupChatJID, String sessionID, String body,
+ String from, Map<String, List<String>> metaData) {
+ WorkgroupInvitation invitation = new WorkgroupInvitation(connection.getUser(), groupChatJID,
+ workgroupJID, sessionID, body, from, metaData);
+
+ synchronized (invitationListeners) {
+ for (WorkgroupInvitationListener listener : invitationListeners) {
+ listener.invitationReceived(invitation);
+ }
+ }
+ }
+
+ private void fireQueueUsersEvent(WorkgroupQueue queue, WorkgroupQueue.Status status,
+ int averageWaitTime, Date oldestEntry, Set<QueueUser> users) {
+ synchronized (queueUsersListeners) {
+ for (QueueUsersListener listener : queueUsersListeners) {
+ if (status != null) {
+ listener.statusUpdated(queue, status);
+ }
+ if (averageWaitTime != -1) {
+ listener.averageWaitTimeUpdated(queue, averageWaitTime);
+ }
+ if (oldestEntry != null) {
+ listener.oldestEntryUpdated(queue, oldestEntry);
+ }
+ if (users != null) {
+ listener.usersUpdated(queue, users);
+ }
+ }
+ }
+ }
+
+ // PacketListener Implementation.
+
+ private void handlePacket(Packet packet) {
+ if (packet instanceof OfferRequestProvider.OfferRequestPacket) {
+ // Acknowledge the IQ set.
+ IQ reply = new IQ() {
+ public String getChildElementXML() {
+ return null;
+ }
+ };
+ reply.setPacketID(packet.getPacketID());
+ reply.setTo(packet.getFrom());
+ reply.setType(IQ.Type.RESULT);
+ connection.sendPacket(reply);
+
+ fireOfferRequestEvent((OfferRequestProvider.OfferRequestPacket)packet);
+ }
+ else if (packet instanceof Presence) {
+ Presence presence = (Presence)packet;
+
+ // The workgroup can send us a number of different presence packets. We
+ // check for different packet extensions to see what type of presence
+ // packet it is.
+
+ String queueName = StringUtils.parseResource(presence.getFrom());
+ WorkgroupQueue queue = queues.get(queueName);
+ // If there isn't already an entry for the queue, create a new one.
+ if (queue == null) {
+ queue = new WorkgroupQueue(queueName);
+ queues.put(queueName, queue);
+ }
+
+ // QueueOverview packet extensions contain basic information about a queue.
+ QueueOverview queueOverview = (QueueOverview)presence.getExtension(QueueOverview.ELEMENT_NAME, QueueOverview.NAMESPACE);
+ if (queueOverview != null) {
+ if (queueOverview.getStatus() == null) {
+ queue.setStatus(WorkgroupQueue.Status.CLOSED);
+ }
+ else {
+ queue.setStatus(queueOverview.getStatus());
+ }
+ queue.setAverageWaitTime(queueOverview.getAverageWaitTime());
+ queue.setOldestEntry(queueOverview.getOldestEntry());
+ // Fire event.
+ fireQueueUsersEvent(queue, queueOverview.getStatus(),
+ queueOverview.getAverageWaitTime(), queueOverview.getOldestEntry(),
+ null);
+ return;
+ }
+
+ // QueueDetails packet extensions contain information about the users in
+ // a queue.
+ QueueDetails queueDetails = (QueueDetails)packet.getExtension(QueueDetails.ELEMENT_NAME, QueueDetails.NAMESPACE);
+ if (queueDetails != null) {
+ queue.setUsers(queueDetails.getUsers());
+ // Fire event.
+ fireQueueUsersEvent(queue, null, -1, null, queueDetails.getUsers());
+ return;
+ }
+
+ // Notify agent packets gives an overview of agent activity in a queue.
+ DefaultPacketExtension notifyAgents = (DefaultPacketExtension)presence.getExtension("notify-agents", "http://jabber.org/protocol/workgroup");
+ if (notifyAgents != null) {
+ int currentChats = Integer.parseInt(notifyAgents.getValue("current-chats"));
+ int maxChats = Integer.parseInt(notifyAgents.getValue("max-chats"));
+ queue.setCurrentChats(currentChats);
+ queue.setMaxChats(maxChats);
+ // Fire event.
+ // TODO: might need another event for current chats and max chats of queue
+ return;
+ }
+ }
+ else if (packet instanceof Message) {
+ Message message = (Message)packet;
+
+ // Check if a room invitation was sent and if the sender is the workgroup
+ MUCUser mucUser = (MUCUser)message.getExtension("x",
+ "http://jabber.org/protocol/muc#user");
+ MUCUser.Invite invite = mucUser != null ? mucUser.getInvite() : null;
+ if (invite != null && workgroupJID.equals(invite.getFrom())) {
+ String sessionID = null;
+ Map<String, List<String>> metaData = null;
+
+ SessionID sessionIDExt = (SessionID)message.getExtension(SessionID.ELEMENT_NAME,
+ SessionID.NAMESPACE);
+ if (sessionIDExt != null) {
+ sessionID = sessionIDExt.getSessionID();
+ }
+
+ MetaData metaDataExt = (MetaData)message.getExtension(MetaData.ELEMENT_NAME,
+ MetaData.NAMESPACE);
+ if (metaDataExt != null) {
+ metaData = metaDataExt.getMetaData();
+ }
+
+ this.fireInvitationEvent(message.getFrom(), sessionID, message.getBody(),
+ message.getFrom(), metaData);
+ }
+ }
+ else if (packet instanceof OfferRevokeProvider.OfferRevokePacket) {
+ // Acknowledge the IQ set.
+ IQ reply = new IQ() {
+ public String getChildElementXML() {
+ return null;
+ }
+ };
+ reply.setPacketID(packet.getPacketID());
+ reply.setType(IQ.Type.RESULT);
+ connection.sendPacket(reply);
+
+ fireOfferRevokeEvent((OfferRevokeProvider.OfferRevokePacket)packet);
+ }
+ }
+
+ /**
+ * Creates a ChatNote that will be mapped to the given chat session.
+ *
+ * @param sessionID the session id of a Chat Session.
+ * @param note the chat note to add.
+ * @throws XMPPException is thrown if an error occurs while adding the note.
+ */
+ public void setNote(String sessionID, String note) throws XMPPException {
+ note = ChatNotes.replace(note, "\n", "\\n");
+ note = StringUtils.escapeForXML(note);
+
+ ChatNotes notes = new ChatNotes();
+ notes.setType(IQ.Type.SET);
+ notes.setTo(workgroupJID);
+ notes.setSessionID(sessionID);
+ notes.setNotes(note);
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(notes.getPacketID()));
+ // Send the request
+ connection.sendPacket(notes);
+
+ IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ }
+
+ /**
+ * Retrieves the ChatNote associated with a given chat session.
+ *
+ * @param sessionID the sessionID of the chat session.
+ * @return the <code>ChatNote</code> associated with a given chat session.
+ * @throws XMPPException if an error occurs while retrieving the ChatNote.
+ */
+ public ChatNotes getNote(String sessionID) throws XMPPException {
+ ChatNotes request = new ChatNotes();
+ request.setType(IQ.Type.GET);
+ request.setTo(workgroupJID);
+ request.setSessionID(sessionID);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+ ChatNotes response = (ChatNotes)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+
+ }
+
+ /**
+ * Retrieves the AgentChatHistory associated with a particular agent jid.
+ *
+ * @param jid the jid of the agent.
+ * @param maxSessions the max number of sessions to retrieve.
+ * @param startDate the starting date of sessions to retrieve.
+ * @return the chat history associated with a given jid.
+ * @throws XMPPException if an error occurs while retrieving the AgentChatHistory.
+ */
+ public AgentChatHistory getAgentHistory(String jid, int maxSessions, Date startDate) throws XMPPException {
+ AgentChatHistory request;
+ if (startDate != null) {
+ request = new AgentChatHistory(jid, maxSessions, startDate);
+ }
+ else {
+ request = new AgentChatHistory(jid, maxSessions);
+ }
+
+ request.setType(IQ.Type.GET);
+ request.setTo(workgroupJID);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+ AgentChatHistory response = (AgentChatHistory)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+ }
+
+ /**
+ * Asks the workgroup for it's Search Settings.
+ *
+ * @return SearchSettings the search settings for this workgroup.
+ * @throws XMPPException if an error occurs while getting information from the server.
+ */
+ public SearchSettings getSearchSettings() throws XMPPException {
+ SearchSettings request = new SearchSettings();
+ request.setType(IQ.Type.GET);
+ request.setTo(workgroupJID);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+
+ SearchSettings response = (SearchSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+ }
+
+ /**
+ * Asks the workgroup for it's Global Macros.
+ *
+ * @param global true to retrieve global macros, otherwise false for personal macros.
+ * @return MacroGroup the root macro group.
+ * @throws XMPPException if an error occurs while getting information from the server.
+ */
+ public MacroGroup getMacros(boolean global) throws XMPPException {
+ Macros request = new Macros();
+ request.setType(IQ.Type.GET);
+ request.setTo(workgroupJID);
+ request.setPersonal(!global);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+
+ Macros response = (Macros)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response.getRootGroup();
+ }
+
+ /**
+ * Persists the Personal Macro for an agent.
+ *
+ * @param group the macro group to save.
+ * @throws XMPPException if an error occurs while getting information from the server.
+ */
+ public void saveMacros(MacroGroup group) throws XMPPException {
+ Macros request = new Macros();
+ request.setType(IQ.Type.SET);
+ request.setTo(workgroupJID);
+ request.setPersonal(true);
+ request.setPersonalMacroGroup(group);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+
+ IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ }
+
+ /**
+ * Query for metadata associated with a session id.
+ *
+ * @param sessionID the sessionID to query for.
+ * @return Map a map of all metadata associated with the sessionID.
+ * @throws XMPPException if an error occurs while getting information from the server.
+ */
+ public Map<String, List<String>> getChatMetadata(String sessionID) throws XMPPException {
+ ChatMetadata request = new ChatMetadata();
+ request.setType(IQ.Type.GET);
+ request.setTo(workgroupJID);
+ request.setSessionID(sessionID);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+
+ ChatMetadata response = (ChatMetadata)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response.getMetadata();
+ }
+
+ /**
+ * Invites a user or agent to an existing session support. The provided invitee's JID can be of
+ * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service
+ * will decide the best agent to receive the invitation.<p>
+ *
+ * This method will return either when the service returned an ACK of the request or if an error occured
+ * while requesting the invitation. After sending the ACK the service will send the invitation to the target
+ * entity. When dealing with agents the common sequence of offer-response will be followed. However, when
+ * sending an invitation to a user a standard MUC invitation will be sent.<p>
+ *
+ * The agent or user that accepted the offer <b>MUST</b> join the room. Failing to do so will make
+ * the invitation to fail. The inviter will eventually receive a message error indicating that the invitee
+ * accepted the offer but failed to join the room.
+ *
+ * Different situations may lead to a failed invitation. Possible cases are: 1) all agents rejected the
+ * offer and ther are no agents available, 2) the agent that accepted the offer failed to join the room or
+ * 2) the user that received the MUC invitation never replied or joined the room. In any of these cases
+ * (or other failing cases) the inviter will get an error message with the failed notification.
+ *
+ * @param type type of entity that will get the invitation.
+ * @param invitee JID of entity that will get the invitation.
+ * @param sessionID ID of the support session that the invitee is being invited.
+ * @param reason the reason of the invitation.
+ * @throws XMPPException if the sender of the invitation is not an agent or the service failed to process
+ * the request.
+ */
+ public void sendRoomInvitation(RoomInvitation.Type type, String invitee, String sessionID, String reason)
+ throws XMPPException {
+ final RoomInvitation invitation = new RoomInvitation(type, invitee, sessionID, reason);
+ IQ iq = new IQ() {
+
+ public String getChildElementXML() {
+ return invitation.toXML();
+ }
+ };
+ iq.setType(IQ.Type.SET);
+ iq.setTo(workgroupJID);
+ iq.setFrom(connection.getUser());
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(iq.getPacketID()));
+ connection.sendPacket(iq);
+
+ IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ }
+
+ /**
+ * Transfer an existing session support to another user or agent. The provided invitee's JID can be of
+ * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service
+ * will decide the best agent to receive the invitation.<p>
+ *
+ * This method will return either when the service returned an ACK of the request or if an error occured
+ * while requesting the transfer. After sending the ACK the service will send the invitation to the target
+ * entity. When dealing with agents the common sequence of offer-response will be followed. However, when
+ * sending an invitation to a user a standard MUC invitation will be sent.<p>
+ *
+ * Once the invitee joins the support room the workgroup service will kick the inviter from the room.<p>
+ *
+ * Different situations may lead to a failed transfers. Possible cases are: 1) all agents rejected the
+ * offer and there are no agents available, 2) the agent that accepted the offer failed to join the room
+ * or 2) the user that received the MUC invitation never replied or joined the room. In any of these cases
+ * (or other failing cases) the inviter will get an error message with the failed notification.
+ *
+ * @param type type of entity that will get the invitation.
+ * @param invitee JID of entity that will get the invitation.
+ * @param sessionID ID of the support session that the invitee is being invited.
+ * @param reason the reason of the invitation.
+ * @throws XMPPException if the sender of the invitation is not an agent or the service failed to process
+ * the request.
+ */
+ public void sendRoomTransfer(RoomTransfer.Type type, String invitee, String sessionID, String reason)
+ throws XMPPException {
+ final RoomTransfer transfer = new RoomTransfer(type, invitee, sessionID, reason);
+ IQ iq = new IQ() {
+
+ public String getChildElementXML() {
+ return transfer.toXML();
+ }
+ };
+ iq.setType(IQ.Type.SET);
+ iq.setTo(workgroupJID);
+ iq.setFrom(connection.getUser());
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(iq.getPacketID()));
+ connection.sendPacket(iq);
+
+ IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ }
+
+ /**
+ * Returns the generic metadata of the workgroup the agent belongs to.
+ *
+ * @param con the Connection to use.
+ * @param query an optional query object used to tell the server what metadata to retrieve. This can be null.
+ * @throws XMPPException if an error occurs while sending the request to the server.
+ * @return the settings for the workgroup.
+ */
+ public GenericSettings getGenericSettings(Connection con, String query) throws XMPPException {
+ GenericSettings setting = new GenericSettings();
+ setting.setType(IQ.Type.GET);
+ setting.setTo(workgroupJID);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(setting.getPacketID()));
+ connection.sendPacket(setting);
+
+ GenericSettings response = (GenericSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+ }
+
+ public boolean hasMonitorPrivileges(Connection con) throws XMPPException {
+ MonitorPacket request = new MonitorPacket();
+ request.setType(IQ.Type.GET);
+ request.setTo(workgroupJID);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+ MonitorPacket response = (MonitorPacket)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response.isMonitor();
+
+ }
+
+ public void makeRoomOwner(Connection con, String sessionID) throws XMPPException {
+ MonitorPacket request = new MonitorPacket();
+ request.setType(IQ.Type.SET);
+ request.setTo(workgroupJID);
+ request.setSessionID(sessionID);
+
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+ Packet response = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/agent/InvitationRequest.java b/src/org/jivesoftware/smackx/workgroup/agent/InvitationRequest.java new file mode 100644 index 0000000..16b324a --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/InvitationRequest.java @@ -0,0 +1,62 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+/**
+ * Request sent by an agent to invite another agent or user.
+ *
+ * @author Gaston Dombiak
+ */
+public class InvitationRequest extends OfferContent {
+
+ private String inviter;
+ private String room;
+ private String reason;
+
+ public InvitationRequest(String inviter, String room, String reason) {
+ this.inviter = inviter;
+ this.room = room;
+ this.reason = reason;
+ }
+
+ public String getInviter() {
+ return inviter;
+ }
+
+ public String getRoom() {
+ return room;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ boolean isUserRequest() {
+ return false;
+ }
+
+ boolean isInvitation() {
+ return true;
+ }
+
+ boolean isTransfer() {
+ return false;
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/Offer.java b/src/org/jivesoftware/smackx/workgroup/agent/Offer.java new file mode 100644 index 0000000..ece8c6f --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/Offer.java @@ -0,0 +1,223 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A class embodying the semantic agent chat offer; specific instances allow the acceptance or
+ * rejecting of the offer.<br>
+ *
+ * @author Matt Tucker
+ * @author loki der quaeler
+ * @author Derek DeMoro
+ */
+public class Offer {
+
+ private Connection connection;
+ private AgentSession session;
+
+ private String sessionID;
+ private String userJID;
+ private String userID;
+ private String workgroupName;
+ private Date expiresDate;
+ private Map<String, List<String>> metaData;
+ private OfferContent content;
+
+ private boolean accepted = false;
+ private boolean rejected = false;
+
+ /**
+ * Creates a new offer.
+ *
+ * @param conn the XMPP connection with which the issuing session was created.
+ * @param agentSession the agent session instance through which this offer was issued.
+ * @param userID the userID of the user from which the offer originates.
+ * @param userJID the XMPP address of the user from which the offer originates.
+ * @param workgroupName the fully qualified name of the workgroup.
+ * @param expiresDate the date at which this offer expires.
+ * @param sessionID the session id associated with the offer.
+ * @param metaData the metadata associated with the offer.
+ * @param content content of the offer. The content explains the reason for the offer
+ * (e.g. user request, transfer)
+ */
+ Offer(Connection conn, AgentSession agentSession, String userID,
+ String userJID, String workgroupName, Date expiresDate,
+ String sessionID, Map<String, List<String>> metaData, OfferContent content)
+ {
+ this.connection = conn;
+ this.session = agentSession;
+ this.userID = userID;
+ this.userJID = userJID;
+ this.workgroupName = workgroupName;
+ this.expiresDate = expiresDate;
+ this.sessionID = sessionID;
+ this.metaData = metaData;
+ this.content = content;
+ }
+
+ /**
+ * Accepts the offer.
+ */
+ public void accept() {
+ Packet acceptPacket = new AcceptPacket(this.session.getWorkgroupJID());
+ connection.sendPacket(acceptPacket);
+ // TODO: listen for a reply.
+ accepted = true;
+ }
+
+ /**
+ * Rejects the offer.
+ */
+ public void reject() {
+ RejectPacket rejectPacket = new RejectPacket(this.session.getWorkgroupJID());
+ connection.sendPacket(rejectPacket);
+ // TODO: listen for a reply.
+ rejected = true;
+ }
+
+ /**
+ * Returns the userID that the offer originates from. In most cases, the
+ * userID will simply be the JID of the requesting user. However, users can
+ * also manually specify a userID for their request. In that case, that value will
+ * be returned.
+ *
+ * @return the userID of the user from which the offer originates.
+ */
+ public String getUserID() {
+ return userID;
+ }
+
+ /**
+ * Returns the JID of the user that made the offer request.
+ *
+ * @return the user's JID.
+ */
+ public String getUserJID() {
+ return userJID;
+ }
+
+ /**
+ * The fully qualified name of the workgroup (eg support@example.com).
+ *
+ * @return the name of the workgroup.
+ */
+ public String getWorkgroupName() {
+ return this.workgroupName;
+ }
+
+ /**
+ * The date when the offer will expire. The agent must {@link #accept()}
+ * the offer before the expiration date or the offer will lapse and be
+ * routed to another agent. Alternatively, the agent can {@link #reject()}
+ * the offer at any time if they don't wish to accept it..
+ *
+ * @return the date at which this offer expires.
+ */
+ public Date getExpiresDate() {
+ return this.expiresDate;
+ }
+
+ /**
+ * The session ID associated with the offer.
+ *
+ * @return the session id associated with the offer.
+ */
+ public String getSessionID() {
+ return this.sessionID;
+ }
+
+ /**
+ * The meta-data associated with the offer.
+ *
+ * @return the offer meta-data.
+ */
+ public Map<String, List<String>> getMetaData() {
+ return this.metaData;
+ }
+
+ /**
+ * Returns the content of the offer. The content explains the reason for the offer
+ * (e.g. user request, transfer)
+ *
+ * @return the content of the offer.
+ */
+ public OfferContent getContent() {
+ return content;
+ }
+
+ /**
+ * Returns true if the agent accepted this offer.
+ *
+ * @return true if the agent accepted this offer.
+ */
+ public boolean isAccepted() {
+ return accepted;
+ }
+
+ /**
+ * Return true if the agent rejected this offer.
+ *
+ * @return true if the agent rejected this offer.
+ */
+ public boolean isRejected() {
+ return rejected;
+ }
+
+ /**
+ * Packet for rejecting offers.
+ */
+ private class RejectPacket extends IQ {
+
+ RejectPacket(String workgroup) {
+ this.setTo(workgroup);
+ this.setType(IQ.Type.SET);
+ }
+
+ public String getChildElementXML() {
+ return "<offer-reject id=\"" + Offer.this.getSessionID() +
+ "\" xmlns=\"http://jabber.org/protocol/workgroup" + "\"/>";
+ }
+ }
+
+ /**
+ * Packet for accepting an offer.
+ */
+ private class AcceptPacket extends IQ {
+
+ AcceptPacket(String workgroup) {
+ this.setTo(workgroup);
+ this.setType(IQ.Type.SET);
+ }
+
+ public String getChildElementXML() {
+ return "<offer-accept id=\"" + Offer.this.getSessionID() +
+ "\" xmlns=\"http://jabber.org/protocol/workgroup" + "\"/>";
+ }
+ }
+
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/agent/OfferConfirmation.java b/src/org/jivesoftware/smackx/workgroup/agent/OfferConfirmation.java new file mode 100644 index 0000000..f55d588 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/OfferConfirmation.java @@ -0,0 +1,114 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+
+public class OfferConfirmation extends IQ {
+ private String userJID;
+ private long sessionID;
+
+ public String getUserJID() {
+ return userJID;
+ }
+
+ public void setUserJID(String userJID) {
+ this.userJID = userJID;
+ }
+
+ public long getSessionID() {
+ return sessionID;
+ }
+
+ public void setSessionID(long sessionID) {
+ this.sessionID = sessionID;
+ }
+
+
+ public void notifyService(Connection con, String workgroup, String createdRoomName) {
+ NotifyServicePacket packet = new NotifyServicePacket(workgroup, createdRoomName);
+ con.sendPacket(packet);
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<offer-confirmation xmlns=\"http://jabber.org/protocol/workgroup\">");
+ buf.append("</offer-confirmation>");
+ return buf.toString();
+ }
+
+ public static class Provider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ final OfferConfirmation confirmation = new OfferConfirmation();
+
+ boolean done = false;
+ while (!done) {
+ parser.next();
+ String elementName = parser.getName();
+ if (parser.getEventType() == XmlPullParser.START_TAG && "user-jid".equals(elementName)) {
+ try {
+ confirmation.setUserJID(parser.nextText());
+ }
+ catch (NumberFormatException nfe) {
+ }
+ }
+ else if (parser.getEventType() == XmlPullParser.START_TAG && "session-id".equals(elementName)) {
+ try {
+ confirmation.setSessionID(Long.valueOf(parser.nextText()));
+ }
+ catch (NumberFormatException nfe) {
+ }
+ }
+ else if (parser.getEventType() == XmlPullParser.END_TAG && "offer-confirmation".equals(elementName)) {
+ done = true;
+ }
+ }
+
+
+ return confirmation;
+ }
+ }
+
+
+ /**
+ * Packet for notifying server of RoomName
+ */
+ private class NotifyServicePacket extends IQ {
+ String roomName;
+
+ NotifyServicePacket(String workgroup, String roomName) {
+ this.setTo(workgroup);
+ this.setType(IQ.Type.RESULT);
+
+ this.roomName = roomName;
+ }
+
+ public String getChildElementXML() {
+ return "<offer-confirmation roomname=\"" + roomName + "\" xmlns=\"http://jabber.org/protocol/workgroup" + "\"/>";
+ }
+ }
+
+
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/OfferConfirmationListener.java b/src/org/jivesoftware/smackx/workgroup/agent/OfferConfirmationListener.java new file mode 100644 index 0000000..fb10550 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/OfferConfirmationListener.java @@ -0,0 +1,32 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+public interface OfferConfirmationListener {
+
+
+ /**
+ * The implementing class instance will be notified via this when the AgentSession has confirmed
+ * the acceptance of the <code>Offer</code>. The instance will then have the ability to create the room and
+ * send the service the room name created for tracking.
+ *
+ * @param confirmedOffer the ConfirmedOffer
+ */
+ void offerConfirmed(OfferConfirmation confirmedOffer);
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/OfferContent.java b/src/org/jivesoftware/smackx/workgroup/agent/OfferContent.java new file mode 100644 index 0000000..a11ddc3 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/OfferContent.java @@ -0,0 +1,55 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+/**
+ * Type of content being included in the offer. The content actually explains the reason
+ * the agent is getting an offer.
+ *
+ * @author Gaston Dombiak
+ */
+public abstract class OfferContent {
+
+ /**
+ * Returns true if the content of the offer is related to a user request. This is the
+ * most common type of offers an agent should receive.
+ *
+ * @return true if the content of the offer is related to a user request.
+ */
+ abstract boolean isUserRequest();
+
+ /**
+ * Returns true if the content of the offer is related to a room invitation made by another
+ * agent. This type of offer include the room to join, metadata sent by the user while joining
+ * the queue and the reason why the agent is being invited.
+ *
+ * @return true if the content of the offer is related to a room invitation made by another agent.
+ */
+ abstract boolean isInvitation();
+
+ /**
+ * Returns true if the content of the offer is related to a service transfer made by another
+ * agent. This type of offers include the room to join, metadata sent by the user while joining the
+ * queue and the reason why the agent is receiving the transfer offer.
+ *
+ * @return true if the content of the offer is related to a service transfer made by another agent.
+ */
+ abstract boolean isTransfer();
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/OfferListener.java b/src/org/jivesoftware/smackx/workgroup/agent/OfferListener.java new file mode 100644 index 0000000..5efde99 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/OfferListener.java @@ -0,0 +1,49 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+/**
+ * An interface which all classes interested in hearing about chat offers associated to a particular
+ * AgentSession instance should implement.<br>
+ *
+ * @author Matt Tucker
+ * @author loki der quaeler
+ * @see org.jivesoftware.smackx.workgroup.agent.AgentSession
+ */
+public interface OfferListener {
+
+ /**
+ * The implementing class instance will be notified via this when the AgentSession has received
+ * an offer for a chat. The instance will then have the ability to accept, reject, or ignore
+ * the request (resulting in a revocation-by-timeout).
+ *
+ * @param request the Offer instance embodying the details of the offer
+ */
+ public void offerReceived (Offer request);
+
+ /**
+ * The implementing class instance will be notified via this when the AgentSessino has received
+ * a revocation of a previously extended offer.
+ *
+ * @param revokedOffer the RevokedOffer instance embodying the details of the revoked offer
+ */
+ public void offerRevoked (RevokedOffer revokedOffer);
+
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/QueueUsersListener.java b/src/org/jivesoftware/smackx/workgroup/agent/QueueUsersListener.java new file mode 100644 index 0000000..9fcff9a --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/QueueUsersListener.java @@ -0,0 +1,60 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+import java.util.Date;
+import java.util.Set;
+
+import org.jivesoftware.smackx.workgroup.QueueUser;
+
+public interface QueueUsersListener {
+
+ /**
+ * The status of the queue was updated.
+ *
+ * @param queue the workgroup queue.
+ * @param status the status of queue.
+ */
+ public void statusUpdated(WorkgroupQueue queue, WorkgroupQueue.Status status);
+
+ /**
+ * The average wait time of the queue was updated.
+ *
+ * @param queue the workgroup queue.
+ * @param averageWaitTime the average wait time of the queue.
+ */
+ public void averageWaitTimeUpdated(WorkgroupQueue queue, int averageWaitTime);
+
+ /**
+ * The date of oldest entry waiting in the queue was updated.
+ *
+ * @param queue the workgroup queue.
+ * @param oldestEntry the date of the oldest entry waiting in the queue.
+ */
+ public void oldestEntryUpdated(WorkgroupQueue queue, Date oldestEntry);
+
+ /**
+ * The list of users waiting in the queue was updated.
+ *
+ * @param queue the workgroup queue.
+ * @param users the list of users waiting in the queue.
+ */
+ public void usersUpdated(WorkgroupQueue queue, Set<QueueUser> users);
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/agent/RevokedOffer.java b/src/org/jivesoftware/smackx/workgroup/agent/RevokedOffer.java new file mode 100644 index 0000000..dab4d91 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/RevokedOffer.java @@ -0,0 +1,98 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+import java.util.Date;
+
+/**
+ * An immutable simple class to embody the information concerning a revoked offer, this is namely
+ * the reason, the workgroup, the userJID, and the timestamp which the message was received.<br>
+ *
+ * @author loki der quaeler
+ */
+public class RevokedOffer {
+
+ private String userJID;
+ private String userID;
+ private String workgroupName;
+ private String sessionID;
+ private String reason;
+ private Date timestamp;
+
+ /**
+ *
+ * @param userJID the JID of the user for which this revocation was issued.
+ * @param userID the user ID of the user for which this revocation was issued.
+ * @param workgroupName the fully qualified name of the workgroup
+ * @param sessionID the session id attributed to this chain of packets
+ * @param reason the server issued message as to why this revocation was issued.
+ * @param timestamp the timestamp at which the revocation was issued
+ */
+ RevokedOffer(String userJID, String userID, String workgroupName, String sessionID,
+ String reason, Date timestamp) {
+ super();
+
+ this.userJID = userJID;
+ this.userID = userID;
+ this.workgroupName = workgroupName;
+ this.sessionID = sessionID;
+ this.reason = reason;
+ this.timestamp = timestamp;
+ }
+
+ public String getUserJID() {
+ return userJID;
+ }
+
+ /**
+ * @return the jid of the user for which this revocation was issued
+ */
+ public String getUserID() {
+ return this.userID;
+ }
+
+ /**
+ * @return the fully qualified name of the workgroup
+ */
+ public String getWorkgroupName() {
+ return this.workgroupName;
+ }
+
+ /**
+ * @return the session id which will associate all packets for the pending chat
+ */
+ public String getSessionID() {
+ return this.sessionID;
+ }
+
+ /**
+ * @return the server issued message as to why this revocation was issued
+ */
+ public String getReason() {
+ return this.reason;
+ }
+
+ /**
+ * @return the timestamp at which the revocation was issued
+ */
+ public Date getTimestamp() {
+ return this.timestamp;
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/agent/TranscriptManager.java b/src/org/jivesoftware/smackx/workgroup/agent/TranscriptManager.java new file mode 100644 index 0000000..8a3801f --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/TranscriptManager.java @@ -0,0 +1,100 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+import org.jivesoftware.smackx.workgroup.packet.Transcript;
+import org.jivesoftware.smackx.workgroup.packet.Transcripts;
+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.PacketIDFilter;
+
+/**
+ * A TranscriptManager helps to retrieve the full conversation transcript of a given session
+ * {@link #getTranscript(String, String)} or to retrieve a list with the summary of all the
+ * conversations that a user had {@link #getTranscripts(String, String)}.
+ *
+ * @author Gaston Dombiak
+ */
+public class TranscriptManager {
+ private Connection connection;
+
+ public TranscriptManager(Connection connection) {
+ this.connection = connection;
+ }
+
+ /**
+ * Returns the full conversation transcript of a given session.
+ *
+ * @param sessionID the id of the session to get the full transcript.
+ * @param workgroupJID the JID of the workgroup that will process the request.
+ * @return the full conversation transcript of a given session.
+ * @throws XMPPException if an error occurs while getting the information.
+ */
+ public Transcript getTranscript(String workgroupJID, String sessionID) throws XMPPException {
+ Transcript request = new Transcript(sessionID);
+ request.setTo(workgroupJID);
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ // Send the request
+ connection.sendPacket(request);
+
+ Transcript response = (Transcript) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+ }
+
+ /**
+ * Returns the transcripts of a given user. The answer will contain the complete history of
+ * conversations that a user had.
+ *
+ * @param userID the id of the user to get his conversations.
+ * @param workgroupJID the JID of the workgroup that will process the request.
+ * @return the transcripts of a given user.
+ * @throws XMPPException if an error occurs while getting the information.
+ */
+ public Transcripts getTranscripts(String workgroupJID, String userID) throws XMPPException {
+ Transcripts request = new Transcripts(userID);
+ request.setTo(workgroupJID);
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ // Send the request
+ connection.sendPacket(request);
+
+ Transcripts response = (Transcripts) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/TranscriptSearchManager.java b/src/org/jivesoftware/smackx/workgroup/agent/TranscriptSearchManager.java new file mode 100644 index 0000000..8260cd6 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/TranscriptSearchManager.java @@ -0,0 +1,111 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+import org.jivesoftware.smackx.workgroup.packet.TranscriptSearch;
+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.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.ReportedData;
+
+/**
+ * A TranscriptSearchManager helps to retrieve the form to use for searching transcripts
+ * {@link #getSearchForm(String)} or to submit a search form and return the results of
+ * the search {@link #submitSearch(String, Form)}.
+ *
+ * @author Gaston Dombiak
+ */
+public class TranscriptSearchManager {
+ private Connection connection;
+
+ public TranscriptSearchManager(Connection connection) {
+ this.connection = connection;
+ }
+
+ /**
+ * Returns the Form to use for searching transcripts. It is unlikely that the server
+ * will change the form (without a restart) so it is safe to keep the returned form
+ * for future submissions.
+ *
+ * @param serviceJID the address of the workgroup service.
+ * @return the Form to use for searching transcripts.
+ * @throws XMPPException if an error occurs while sending the request to the server.
+ */
+ public Form getSearchForm(String serviceJID) throws XMPPException {
+ TranscriptSearch search = new TranscriptSearch();
+ search.setType(IQ.Type.GET);
+ search.setTo(serviceJID);
+
+ PacketCollector collector = connection.createPacketCollector(
+ new PacketIDFilter(search.getPacketID()));
+ connection.sendPacket(search);
+
+ TranscriptSearch response = (TranscriptSearch) collector.nextResult(
+ SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return Form.getFormFrom(response);
+ }
+
+ /**
+ * Submits the completed form and returns the result of the transcript search. The result
+ * will include all the data returned from the server so be careful with the amount of
+ * data that the search may return.
+ *
+ * @param serviceJID the address of the workgroup service.
+ * @param completedForm the filled out search form.
+ * @return the result of the transcript search.
+ * @throws XMPPException if an error occurs while submiting the search to the server.
+ */
+ public ReportedData submitSearch(String serviceJID, Form completedForm) throws XMPPException {
+ TranscriptSearch search = new TranscriptSearch();
+ search.setType(IQ.Type.GET);
+ search.setTo(serviceJID);
+ search.addExtension(completedForm.getDataFormToSend());
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(search.getPacketID()));
+ connection.sendPacket(search);
+
+ TranscriptSearch response = (TranscriptSearch) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return ReportedData.getReportedDataFrom(response);
+ }
+}
+
+
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/TransferRequest.java b/src/org/jivesoftware/smackx/workgroup/agent/TransferRequest.java new file mode 100644 index 0000000..a3abbaa --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/TransferRequest.java @@ -0,0 +1,62 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+/**
+ * Request sent by an agent to transfer a support session to another agent or user.
+ *
+ * @author Gaston Dombiak
+ */
+public class TransferRequest extends OfferContent {
+
+ private String inviter;
+ private String room;
+ private String reason;
+
+ public TransferRequest(String inviter, String room, String reason) {
+ this.inviter = inviter;
+ this.room = room;
+ this.reason = reason;
+ }
+
+ public String getInviter() {
+ return inviter;
+ }
+
+ public String getRoom() {
+ return room;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ boolean isUserRequest() {
+ return false;
+ }
+
+ boolean isInvitation() {
+ return false;
+ }
+
+ boolean isTransfer() {
+ return true;
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/UserRequest.java b/src/org/jivesoftware/smackx/workgroup/agent/UserRequest.java new file mode 100644 index 0000000..ccaaaf3 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/UserRequest.java @@ -0,0 +1,47 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+/**
+ * Requests made by users to get support by some agent.
+ *
+ * @author Gaston Dombiak
+ */
+public class UserRequest extends OfferContent {
+ // TODO Do we want to use a singleton? Should we store the userID here?
+ private static UserRequest instance = new UserRequest();
+
+ public static OfferContent getInstance() {
+ return instance;
+ }
+
+ boolean isUserRequest() {
+ return true;
+ }
+
+ boolean isInvitation() {
+ return false;
+ }
+
+ boolean isTransfer() {
+ return false;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/WorkgroupQueue.java b/src/org/jivesoftware/smackx/workgroup/agent/WorkgroupQueue.java new file mode 100644 index 0000000..b43c826 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/agent/WorkgroupQueue.java @@ -0,0 +1,224 @@ +/**
+ * $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.smackx.workgroup.agent;
+
+import java.util.*;
+
+import org.jivesoftware.smackx.workgroup.QueueUser;
+
+/**
+ * A queue in a workgroup, which is a pool of agents that are routed a specific type of
+ * chat request.
+ */
+public class WorkgroupQueue {
+
+ private String name;
+ private Status status = Status.CLOSED;
+
+ private int averageWaitTime = -1;
+ private Date oldestEntry = null;
+ private Set<QueueUser> users = Collections.emptySet();
+
+ private int maxChats = 0;
+ private int currentChats = 0;
+
+ /**
+ * Creates a new workgroup queue instance.
+ *
+ * @param name the name of the queue.
+ */
+ WorkgroupQueue(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the name of the queue.
+ *
+ * @return the name of the queue.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the status of the queue.
+ *
+ * @return the status of the queue.
+ */
+ public Status getStatus() {
+ return status;
+ }
+
+ void setStatus(Status status) {
+ this.status = status;
+ }
+
+ /**
+ * Returns the number of users waiting in the queue waiting to be routed to
+ * an agent.
+ *
+ * @return the number of users waiting in the queue.
+ */
+ public int getUserCount() {
+ if (users == null) {
+ return 0;
+ }
+ return users.size();
+ }
+
+ /**
+ * Returns an Iterator for the users in the queue waiting to be routed to
+ * an agent (QueueUser instances).
+ *
+ * @return an Iterator for the users waiting in the queue.
+ */
+ public Iterator<QueueUser> getUsers() {
+ if (users == null) {
+ return new HashSet<QueueUser>().iterator();
+ }
+ return Collections.unmodifiableSet(users).iterator();
+ }
+
+ void setUsers(Set<QueueUser> users) {
+ this.users = users;
+ }
+
+ /**
+ * Returns the average amount of time users wait in the queue before being
+ * routed to an agent. If average wait time info isn't available, -1 will
+ * be returned.
+ *
+ * @return the average wait time
+ */
+ public int getAverageWaitTime() {
+ return averageWaitTime;
+ }
+
+ void setAverageWaitTime(int averageTime) {
+ this.averageWaitTime = averageTime;
+ }
+
+ /**
+ * Returns the date of the oldest request waiting in the queue. If there
+ * are no requests waiting to be routed, this method will return <tt>null</tt>.
+ *
+ * @return the date of the oldest request in the queue.
+ */
+ public Date getOldestEntry() {
+ return oldestEntry;
+ }
+
+ void setOldestEntry(Date oldestEntry) {
+ this.oldestEntry = oldestEntry;
+ }
+
+ /**
+ * Returns the maximum number of simultaneous chats the queue can handle.
+ *
+ * @return the max number of chats the queue can handle.
+ */
+ public int getMaxChats() {
+ return maxChats;
+ }
+
+ void setMaxChats(int maxChats) {
+ this.maxChats = maxChats;
+ }
+
+ /**
+ * Returns the current number of active chat sessions in the queue.
+ *
+ * @return the current number of active chat sessions in the queue.
+ */
+ public int getCurrentChats() {
+ return currentChats;
+ }
+
+ void setCurrentChats(int currentChats) {
+ this.currentChats = currentChats;
+ }
+
+ /**
+ * A class to represent the status of the workgroup. The possible values are:
+ *
+ * <ul>
+ * <li>WorkgroupQueue.Status.OPEN -- the queue is active and accepting new chat requests.
+ * <li>WorkgroupQueue.Status.ACTIVE -- the queue is active but NOT accepting new chat
+ * requests.
+ * <li>WorkgroupQueue.Status.CLOSED -- the queue is NOT active and NOT accepting new
+ * chat requests.
+ * </ul>
+ */
+ public static class Status {
+
+ /**
+ * The queue is active and accepting new chat requests.
+ */
+ public static final Status OPEN = new Status("open");
+
+ /**
+ * The queue is active but NOT accepting new chat requests. This state might
+ * occur when the workgroup has closed because regular support hours have closed,
+ * but there are still several requests left in the queue.
+ */
+ public static final Status ACTIVE = new Status("active");
+
+ /**
+ * The queue is NOT active and NOT accepting new chat requests.
+ */
+ public static final Status CLOSED = new Status("closed");
+
+ /**
+ * Converts a String into the corresponding status. Valid String values
+ * that can be converted to a status are: "open", "active", and "closed".
+ *
+ * @param type the String value to covert.
+ * @return the corresponding Type.
+ */
+ public static Status fromString(String type) {
+ if (type == null) {
+ return null;
+ }
+ type = type.toLowerCase();
+ if (OPEN.toString().equals(type)) {
+ return OPEN;
+ }
+ else if (ACTIVE.toString().equals(type)) {
+ return ACTIVE;
+ }
+ else if (CLOSED.toString().equals(type)) {
+ return CLOSED;
+ }
+ else {
+ return null;
+ }
+ }
+
+ private String value;
+
+ private Status(String value) {
+ this.value = value;
+ }
+
+ public String toString() {
+ return value;
+ }
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/ext/forms/WorkgroupForm.java b/src/org/jivesoftware/smackx/workgroup/ext/forms/WorkgroupForm.java new file mode 100644 index 0000000..f2dc08e --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/ext/forms/WorkgroupForm.java @@ -0,0 +1,82 @@ +/**
+ * $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.smackx.workgroup.ext.forms;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.xmlpull.v1.XmlPullParser;
+
+public class WorkgroupForm extends IQ {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "workgroup-form";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");
+ // Add packet extensions, if any are defined.
+ buf.append(getExtensionsXML());
+ buf.append("</").append(ELEMENT_NAME).append("> ");
+
+ return buf.toString();
+ }
+
+ /**
+ * An IQProvider for WebForm packets.
+ *
+ * @author Derek DeMoro
+ */
+ public static class InternalProvider implements IQProvider {
+
+ public InternalProvider() {
+ super();
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ WorkgroupForm answer = new WorkgroupForm();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ // Parse the packet extension
+ answer.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(),
+ parser.getNamespace(), parser));
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(ELEMENT_NAME)) {
+ done = true;
+ }
+ }
+ }
+
+ return answer;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/ext/history/AgentChatHistory.java b/src/org/jivesoftware/smackx/workgroup/ext/history/AgentChatHistory.java new file mode 100644 index 0000000..7b8d200 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/ext/history/AgentChatHistory.java @@ -0,0 +1,155 @@ +/**
+ * $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.smackx.workgroup.ext.history;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * IQ provider used to retrieve individual agent information. Each chat session can be mapped
+ * to one or more jids and therefore retrievable.
+ */
+public class AgentChatHistory extends IQ {
+ private String agentJID;
+ private int maxSessions;
+ private long startDate;
+
+ private List<AgentChatSession> agentChatSessions = new ArrayList<AgentChatSession>();
+
+ public AgentChatHistory(String agentJID, int maxSessions, Date startDate) {
+ this.agentJID = agentJID;
+ this.maxSessions = maxSessions;
+ this.startDate = startDate.getTime();
+ }
+
+ public AgentChatHistory(String agentJID, int maxSessions) {
+ this.agentJID = agentJID;
+ this.maxSessions = maxSessions;
+ this.startDate = 0;
+ }
+
+ public AgentChatHistory() {
+ }
+
+ public void addChatSession(AgentChatSession chatSession) {
+ agentChatSessions.add(chatSession);
+ }
+
+ public Collection<AgentChatSession> getAgentChatSessions() {
+ return agentChatSessions;
+ }
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "chat-sessions";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=");
+ buf.append('"');
+ buf.append(NAMESPACE);
+ buf.append('"');
+ buf.append(" agentJID=\"" + agentJID + "\"");
+ buf.append(" maxSessions=\"" + maxSessions + "\"");
+ buf.append(" startDate=\"" + startDate + "\"");
+
+ buf.append("></").append(ELEMENT_NAME).append("> ");
+ return buf.toString();
+ }
+
+ /**
+ * Packet extension provider for AgentHistory packets.
+ */
+ public static class InternalProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ throw new IllegalStateException("Parser not in proper position, or bad XML.");
+ }
+
+ AgentChatHistory agentChatHistory = new AgentChatHistory();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if ((eventType == XmlPullParser.START_TAG) && ("chat-session".equals(parser.getName()))) {
+ agentChatHistory.addChatSession(parseChatSetting(parser));
+
+ }
+ else if (eventType == XmlPullParser.END_TAG && ELEMENT_NAME.equals(parser.getName())) {
+ done = true;
+ }
+ }
+ return agentChatHistory;
+ }
+
+ private AgentChatSession parseChatSetting(XmlPullParser parser) throws Exception {
+
+ boolean done = false;
+ Date date = null;
+ long duration = 0;
+ String visitorsName = null;
+ String visitorsEmail = null;
+ String sessionID = null;
+ String question = null;
+
+ while (!done) {
+ int eventType = parser.next();
+ if ((eventType == XmlPullParser.START_TAG) && ("date".equals(parser.getName()))) {
+ String dateStr = parser.nextText();
+ long l = Long.valueOf(dateStr).longValue();
+ date = new Date(l);
+ }
+ else if ((eventType == XmlPullParser.START_TAG) && ("duration".equals(parser.getName()))) {
+ duration = Long.valueOf(parser.nextText()).longValue();
+ }
+ else if ((eventType == XmlPullParser.START_TAG) && ("visitorsName".equals(parser.getName()))) {
+ visitorsName = parser.nextText();
+ }
+ else if ((eventType == XmlPullParser.START_TAG) && ("visitorsEmail".equals(parser.getName()))) {
+ visitorsEmail = parser.nextText();
+ }
+ else if ((eventType == XmlPullParser.START_TAG) && ("sessionID".equals(parser.getName()))) {
+ sessionID = parser.nextText();
+ }
+ else if ((eventType == XmlPullParser.START_TAG) && ("question".equals(parser.getName()))) {
+ question = parser.nextText();
+ }
+ else if (eventType == XmlPullParser.END_TAG && "chat-session".equals(parser.getName())) {
+ done = true;
+ }
+ }
+ return new AgentChatSession(date, duration, visitorsName, visitorsEmail, sessionID, question);
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/ext/history/AgentChatSession.java b/src/org/jivesoftware/smackx/workgroup/ext/history/AgentChatSession.java new file mode 100644 index 0000000..5113cda --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/ext/history/AgentChatSession.java @@ -0,0 +1,93 @@ +/**
+ * $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.smackx.workgroup.ext.history;
+
+import java.util.Date;
+
+/**
+ * Represents one chat session for an agent.
+ */
+public class AgentChatSession {
+ public Date startDate;
+ public long duration;
+ public String visitorsName;
+ public String visitorsEmail;
+ public String sessionID;
+ public String question;
+
+ public AgentChatSession(Date date, long duration, String visitorsName, String visitorsEmail, String sessionID, String question) {
+ this.startDate = date;
+ this.duration = duration;
+ this.visitorsName = visitorsName;
+ this.visitorsEmail = visitorsEmail;
+ this.sessionID = sessionID;
+ this.question = question;
+ }
+
+ public Date getStartDate() {
+ return startDate;
+ }
+
+ public void setStartDate(Date startDate) {
+ this.startDate = startDate;
+ }
+
+ public long getDuration() {
+ return duration;
+ }
+
+ public void setDuration(long duration) {
+ this.duration = duration;
+ }
+
+ public String getVisitorsName() {
+ return visitorsName;
+ }
+
+ public void setVisitorsName(String visitorsName) {
+ this.visitorsName = visitorsName;
+ }
+
+ public String getVisitorsEmail() {
+ return visitorsEmail;
+ }
+
+ public void setVisitorsEmail(String visitorsEmail) {
+ this.visitorsEmail = visitorsEmail;
+ }
+
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ public void setSessionID(String sessionID) {
+ this.sessionID = sessionID;
+ }
+
+ public void setQuestion(String question){
+ this.question = question;
+ }
+
+ public String getQuestion(){
+ return question;
+ }
+
+
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/ext/history/ChatMetadata.java b/src/org/jivesoftware/smackx/workgroup/ext/history/ChatMetadata.java new file mode 100644 index 0000000..301e1a5 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/ext/history/ChatMetadata.java @@ -0,0 +1,116 @@ +/**
+ * $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.smackx.workgroup.ext.history;
+
+import org.jivesoftware.smackx.workgroup.util.MetaDataUtils;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ChatMetadata extends IQ {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "chat-metadata";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+
+ private String sessionID;
+
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ public void setSessionID(String sessionID) {
+ this.sessionID = sessionID;
+ }
+
+
+ private Map<String, List<String>> map = new HashMap<String, List<String>>();
+
+ public void setMetadata(Map<String, List<String>> metadata){
+ this.map = metadata;
+ }
+
+ public Map<String, List<String>> getMetadata(){
+ return map;
+ }
+
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");
+ buf.append("<sessionID>").append(getSessionID()).append("</sessionID>");
+ buf.append("</").append(ELEMENT_NAME).append("> ");
+
+ return buf.toString();
+ }
+
+ /**
+ * An IQProvider for Metadata packets.
+ *
+ * @author Derek DeMoro
+ */
+ public static class Provider implements IQProvider {
+
+ public Provider() {
+ super();
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ final ChatMetadata chatM = new ChatMetadata();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("sessionID")) {
+ chatM.setSessionID(parser.nextText());
+ }
+ else if (parser.getName().equals("metadata")) {
+ Map<String, List<String>> map = MetaDataUtils.parseMetaData(parser);
+ chatM.setMetadata(map);
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(ELEMENT_NAME)) {
+ done = true;
+ }
+ }
+ }
+
+ return chatM;
+ }
+ }
+}
+
+
+
+
diff --git a/src/org/jivesoftware/smackx/workgroup/ext/macros/Macro.java b/src/org/jivesoftware/smackx/workgroup/ext/macros/Macro.java new file mode 100644 index 0000000..acf6196 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/ext/macros/Macro.java @@ -0,0 +1,68 @@ +/**
+ * $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.smackx.workgroup.ext.macros;
+
+/**
+ * Macro datamodel.
+ */
+public class Macro {
+ public static final int TEXT = 0;
+ public static final int URL = 1;
+ public static final int IMAGE = 2;
+
+
+ private String title;
+ private String description;
+ private String response;
+ private int type;
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getResponse() {
+ return response;
+ }
+
+ public void setResponse(String response) {
+ this.response = response;
+ }
+
+ public int getType() {
+ return type;
+ }
+
+ public void setType(int type) {
+ this.type = type;
+ }
+
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/ext/macros/MacroGroup.java b/src/org/jivesoftware/smackx/workgroup/ext/macros/MacroGroup.java new file mode 100644 index 0000000..0742b3d --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/ext/macros/MacroGroup.java @@ -0,0 +1,143 @@ +/**
+ * $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.smackx.workgroup.ext.macros;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * MacroGroup datamodel.
+ */
+public class MacroGroup {
+ private List<Macro> macros;
+ private List<MacroGroup> macroGroups;
+
+
+ // Define MacroGroup
+ private String title;
+
+ public MacroGroup() {
+ macros = new ArrayList<Macro>();
+ macroGroups = new ArrayList<MacroGroup>();
+ }
+
+ public void addMacro(Macro macro) {
+ macros.add(macro);
+ }
+
+ public void removeMacro(Macro macro) {
+ macros.remove(macro);
+ }
+
+ public Macro getMacroByTitle(String title) {
+ Collection<Macro> col = Collections.unmodifiableList(macros);
+ Iterator<Macro> iter = col.iterator();
+ while (iter.hasNext()) {
+ Macro macro = (Macro)iter.next();
+ if (macro.getTitle().equalsIgnoreCase(title)) {
+ return macro;
+ }
+ }
+ return null;
+ }
+
+ public void addMacroGroup(MacroGroup group) {
+ macroGroups.add(group);
+ }
+
+ public void removeMacroGroup(MacroGroup group) {
+ macroGroups.remove(group);
+ }
+
+ public Macro getMacro(int location) {
+ return (Macro)macros.get(location);
+ }
+
+ public MacroGroup getMacroGroupByTitle(String title) {
+ Collection<MacroGroup> col = Collections.unmodifiableList(macroGroups);
+ Iterator<MacroGroup> iter = col.iterator();
+ while (iter.hasNext()) {
+ MacroGroup group = (MacroGroup)iter.next();
+ if (group.getTitle().equalsIgnoreCase(title)) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ public MacroGroup getMacroGroup(int location) {
+ return (MacroGroup)macroGroups.get(location);
+ }
+
+
+ public List<Macro> getMacros() {
+ return macros;
+ }
+
+ public void setMacros(List<Macro> macros) {
+ this.macros = macros;
+ }
+
+ public List<MacroGroup> getMacroGroups() {
+ return macroGroups;
+ }
+
+ public void setMacroGroups(List<MacroGroup> macroGroups) {
+ this.macroGroups = macroGroups;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<macrogroup>");
+ buf.append("<title>" + getTitle() + "</title>");
+ buf.append("<macros>");
+ for (Macro macro : getMacros())
+ {
+ buf.append("<macro>");
+ buf.append("<title>" + macro.getTitle() + "</title>");
+ buf.append("<type>" + macro.getType() + "</type>");
+ buf.append("<description>" + macro.getDescription() + "</description>");
+ buf.append("<response>" + macro.getResponse() + "</response>");
+ buf.append("</macro>");
+ }
+ buf.append("</macros>");
+
+ if (getMacroGroups().size() > 0) {
+ buf.append("<macroGroups>");
+ for (MacroGroup groups : getMacroGroups()) {
+ buf.append(groups.toXML());
+ }
+ buf.append("</macroGroups>");
+ }
+ buf.append("</macrogroup>");
+ return buf.toString();
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/ext/macros/Macros.java b/src/org/jivesoftware/smackx/workgroup/ext/macros/Macros.java new file mode 100644 index 0000000..869ec57 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/ext/macros/Macros.java @@ -0,0 +1,198 @@ +/**
+ * $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.smackx.workgroup.ext.macros;
+
+import java.io.StringReader;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.StringUtils;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Macros iq is responsible for handling global and personal macros in the a Live Assistant
+ * Workgroup.
+ */
+public class Macros extends IQ {
+
+ private MacroGroup rootGroup;
+ private boolean personal;
+ private MacroGroup personalMacroGroup;
+
+ public MacroGroup getRootGroup() {
+ return rootGroup;
+ }
+
+ public void setRootGroup(MacroGroup rootGroup) {
+ this.rootGroup = rootGroup;
+ }
+
+ public boolean isPersonal() {
+ return personal;
+ }
+
+ public void setPersonal(boolean personal) {
+ this.personal = personal;
+ }
+
+ public MacroGroup getPersonalMacroGroup() {
+ return personalMacroGroup;
+ }
+
+ public void setPersonalMacroGroup(MacroGroup personalMacroGroup) {
+ this.personalMacroGroup = personalMacroGroup;
+ }
+
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "macros";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");
+ if (isPersonal()) {
+ buf.append("<personal>true</personal>");
+ }
+ if (getPersonalMacroGroup() != null) {
+ buf.append("<personalMacro>");
+ buf.append(StringUtils.escapeForXML(getPersonalMacroGroup().toXML()));
+ buf.append("</personalMacro>");
+ }
+ buf.append("</").append(ELEMENT_NAME).append("> ");
+
+ return buf.toString();
+ }
+
+ /**
+ * An IQProvider for Macro packets.
+ *
+ * @author Derek DeMoro
+ */
+ public static class InternalProvider implements IQProvider {
+
+ public InternalProvider() {
+ super();
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ Macros macroGroup = new Macros();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("model")) {
+ String macros = parser.nextText();
+ MacroGroup group = parseMacroGroups(macros);
+ macroGroup.setRootGroup(group);
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(ELEMENT_NAME)) {
+ done = true;
+ }
+ }
+ }
+
+ return macroGroup;
+ }
+
+ public Macro parseMacro(XmlPullParser parser) throws Exception {
+ Macro macro = new Macro();
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("title")) {
+ parser.next();
+ macro.setTitle(parser.getText());
+ }
+ else if (parser.getName().equals("description")) {
+ macro.setDescription(parser.nextText());
+ }
+ else if (parser.getName().equals("response")) {
+ macro.setResponse(parser.nextText());
+ }
+ else if (parser.getName().equals("type")) {
+ macro.setType(Integer.valueOf(parser.nextText()).intValue());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("macro")) {
+ done = true;
+ }
+ }
+ }
+ return macro;
+ }
+
+ public MacroGroup parseMacroGroup(XmlPullParser parser) throws Exception {
+ MacroGroup group = new MacroGroup();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("macrogroup")) {
+ group.addMacroGroup(parseMacroGroup(parser));
+ }
+ if (parser.getName().equals("title")) {
+ group.setTitle(parser.nextText());
+ }
+ if (parser.getName().equals("macro")) {
+ group.addMacro(parseMacro(parser));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("macrogroup")) {
+ done = true;
+ }
+ }
+ }
+ return group;
+ }
+
+ public MacroGroup parseMacroGroups(String macros) throws Exception {
+
+ MacroGroup group = null;
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+ parser.setInput(new StringReader(macros));
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("macrogroup")) {
+ group = parseMacroGroup(parser);
+ }
+ }
+ }
+ return group;
+ }
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/ext/notes/ChatNotes.java b/src/org/jivesoftware/smackx/workgroup/ext/notes/ChatNotes.java new file mode 100644 index 0000000..eff3c6c --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/ext/notes/ChatNotes.java @@ -0,0 +1,155 @@ +/**
+ * $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.smackx.workgroup.ext.notes;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * IQ packet for retrieving and adding Chat Notes.
+ */
+public class ChatNotes extends IQ {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "chat-notes";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+
+ private String sessionID;
+ private String notes;
+
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ public void setSessionID(String sessionID) {
+ this.sessionID = sessionID;
+ }
+
+ public String getNotes() {
+ return notes;
+ }
+
+ public void setNotes(String notes) {
+ this.notes = notes;
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");
+ buf.append("<sessionID>").append(getSessionID()).append("</sessionID>");
+
+ if (getNotes() != null) {
+ buf.append("<notes>").append(getNotes()).append("</notes>");
+ }
+ buf.append("</").append(ELEMENT_NAME).append("> ");
+
+ return buf.toString();
+ }
+
+ /**
+ * An IQProvider for ChatNotes packets.
+ *
+ * @author Derek DeMoro
+ */
+ public static class Provider implements IQProvider {
+
+ public Provider() {
+ super();
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ ChatNotes chatNotes = new ChatNotes();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("sessionID")) {
+ chatNotes.setSessionID(parser.nextText());
+ }
+ else if (parser.getName().equals("text")) {
+ String note = parser.nextText();
+ note = note.replaceAll("\\\\n", "\n");
+ chatNotes.setNotes(note);
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(ELEMENT_NAME)) {
+ done = true;
+ }
+ }
+ }
+
+ return chatNotes;
+ }
+ }
+
+ /**
+ * Replaces all instances of oldString with newString in string.
+ *
+ * @param string the String to search to perform replacements on
+ * @param oldString the String that should be replaced by newString
+ * @param newString the String that will replace all instances of oldString
+ * @return a String will all instances of oldString replaced by newString
+ */
+ public static final String replace(String string, String oldString, String newString) {
+ if (string == null) {
+ return null;
+ }
+ // If the newString is null or zero length, just return the string since there's nothing
+ // to replace.
+ if (newString == null) {
+ return string;
+ }
+ int i = 0;
+ // Make sure that oldString appears at least once before doing any processing.
+ if ((i = string.indexOf(oldString, i)) >= 0) {
+ // Use char []'s, as they are more efficient to deal with.
+ char[] string2 = string.toCharArray();
+ char[] newString2 = newString.toCharArray();
+ int oLength = oldString.length();
+ StringBuilder buf = new StringBuilder(string2.length);
+ buf.append(string2, 0, i).append(newString2);
+ i += oLength;
+ int j = i;
+ // Replace all remaining instances of oldString with newString.
+ while ((i = string.indexOf(oldString, i)) > 0) {
+ buf.append(string2, j, i - j).append(newString2);
+ i += oLength;
+ j = i;
+ }
+ buf.append(string2, j, string2.length - j);
+ return buf.toString();
+ }
+ return string;
+ }
+}
+
+
+
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/AgentInfo.java b/src/org/jivesoftware/smackx/workgroup/packet/AgentInfo.java new file mode 100644 index 0000000..8b9d230 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/AgentInfo.java @@ -0,0 +1,132 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * IQ packet for retrieving and changing the Agent personal information.
+ */
+public class AgentInfo extends IQ {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "agent-info";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+ private String jid;
+ private String name;
+
+ /**
+ * Returns the Agent's jid.
+ *
+ * @return the Agent's jid.
+ */
+ public String getJid() {
+ return jid;
+ }
+
+ /**
+ * Sets the Agent's jid.
+ *
+ * @param jid the jid of the agent.
+ */
+ public void setJid(String jid) {
+ this.jid = jid;
+ }
+
+ /**
+ * Returns the Agent's name. The name of the agent may be different than the user's name.
+ * This property may be shown in the webchat client.
+ *
+ * @return the Agent's name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the Agent's name. The name of the agent may be different than the user's name.
+ * This property may be shown in the webchat client.
+ *
+ * @param name the new name of the agent.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");
+ if (jid != null) {
+ buf.append("<jid>").append(getJid()).append("</jid>");
+ }
+ if (name != null) {
+ buf.append("<name>").append(getName()).append("</name>");
+ }
+ buf.append("</").append(ELEMENT_NAME).append("> ");
+
+ return buf.toString();
+ }
+
+ /**
+ * An IQProvider for AgentInfo packets.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Provider implements IQProvider {
+
+ public Provider() {
+ super();
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ AgentInfo answer = new AgentInfo();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("jid")) {
+ answer.setJid(parser.nextText());
+ }
+ else if (parser.getName().equals("name")) {
+ answer.setName(parser.nextText());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(ELEMENT_NAME)) {
+ done = true;
+ }
+ }
+ }
+
+ return answer;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/AgentStatus.java b/src/org/jivesoftware/smackx/workgroup/packet/AgentStatus.java new file mode 100644 index 0000000..9f49033 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/AgentStatus.java @@ -0,0 +1,266 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * Agent status packet.
+ *
+ * @author Matt Tucker
+ */
+public class AgentStatus implements PacketExtension {
+
+ private static final SimpleDateFormat UTC_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
+
+ static {
+ UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+0"));
+ }
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "agent-status";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";
+
+ private String workgroupJID;
+ private List<ChatInfo> currentChats = new ArrayList<ChatInfo>();
+ private int maxChats = -1;
+
+ AgentStatus() {
+ }
+
+ public String getWorkgroupJID() {
+ return workgroupJID;
+ }
+
+ /**
+ * Returns a collection of ChatInfo where each ChatInfo represents a Chat where this agent
+ * is participating.
+ *
+ * @return a collection of ChatInfo where each ChatInfo represents a Chat where this agent
+ * is participating.
+ */
+ public List<ChatInfo> getCurrentChats() {
+ return Collections.unmodifiableList(currentChats);
+ }
+
+ public int getMaxChats() {
+ return maxChats;
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\"");
+ if (workgroupJID != null) {
+ buf.append(" jid=\"").append(workgroupJID).append("\"");
+ }
+ buf.append(">");
+ if (maxChats != -1) {
+ buf.append("<max-chats>").append(maxChats).append("</max-chats>");
+ }
+ if (!currentChats.isEmpty()) {
+ buf.append("<current-chats xmlns= \"http://jivesoftware.com/protocol/workgroup\">");
+ for (Iterator<ChatInfo> it = currentChats.iterator(); it.hasNext();) {
+ buf.append(((ChatInfo)it.next()).toXML());
+ }
+ buf.append("</current-chats>");
+ }
+ buf.append("</").append(this.getElementName()).append("> ");
+
+ return buf.toString();
+ }
+
+ /**
+ * Represents information about a Chat where this Agent is participating.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class ChatInfo {
+
+ private String sessionID;
+ private String userID;
+ private Date date;
+ private String email;
+ private String username;
+ private String question;
+
+ public ChatInfo(String sessionID, String userID, Date date, String email, String username, String question) {
+ this.sessionID = sessionID;
+ this.userID = userID;
+ this.date = date;
+ this.email = email;
+ this.username = username;
+ this.question = question;
+ }
+
+ /**
+ * Returns the sessionID associated to this chat. Each chat will have a unique sessionID
+ * that could be used for retrieving the whole transcript of the conversation.
+ *
+ * @return the sessionID associated to this chat.
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ /**
+ * Returns the user unique identification of the user that made the initial request and
+ * for which this chat was generated. If the user joined using an anonymous connection
+ * then the userID will be the value of the ID attribute of the USER element. Otherwise,
+ * the userID will be the bare JID of the user that made the request.
+ *
+ * @return the user unique identification of the user that made the initial request.
+ */
+ public String getUserID() {
+ return userID;
+ }
+
+ /**
+ * Returns the date when this agent joined the chat.
+ *
+ * @return the date when this agent joined the chat.
+ */
+ public Date getDate() {
+ return date;
+ }
+
+ /**
+ * Returns the email address associated with the user.
+ *
+ * @return the email address associated with the user.
+ */
+ public String getEmail() {
+ return email;
+ }
+
+ /**
+ * Returns the username(nickname) associated with the user.
+ *
+ * @return the username associated with the user.
+ */
+ public String getUsername() {
+ return username;
+ }
+
+ /**
+ * Returns the question the user asked.
+ *
+ * @return the question the user asked, if any.
+ */
+ public String getQuestion() {
+ return question;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<chat ");
+ if (sessionID != null) {
+ buf.append(" sessionID=\"").append(sessionID).append("\"");
+ }
+ if (userID != null) {
+ buf.append(" userID=\"").append(userID).append("\"");
+ }
+ if (date != null) {
+ buf.append(" startTime=\"").append(UTC_FORMAT.format(date)).append("\"");
+ }
+ if (email != null) {
+ buf.append(" email=\"").append(email).append("\"");
+ }
+ if (username != null) {
+ buf.append(" username=\"").append(username).append("\"");
+ }
+ if (question != null) {
+ buf.append(" question=\"").append(question).append("\"");
+ }
+ buf.append("/>");
+
+ return buf.toString();
+ }
+ }
+
+ /**
+ * Packet extension provider for AgentStatus packets.
+ */
+ public static class Provider implements PacketExtensionProvider {
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ AgentStatus agentStatus = new AgentStatus();
+
+ agentStatus.workgroupJID = parser.getAttributeValue("", "jid");
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ if ("chat".equals(parser.getName())) {
+ agentStatus.currentChats.add(parseChatInfo(parser));
+ }
+ else if ("max-chats".equals(parser.getName())) {
+ agentStatus.maxChats = Integer.parseInt(parser.nextText());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG &&
+ ELEMENT_NAME.equals(parser.getName())) {
+ done = true;
+ }
+ }
+ return agentStatus;
+ }
+
+ private ChatInfo parseChatInfo(XmlPullParser parser) {
+
+ String sessionID = parser.getAttributeValue("", "sessionID");
+ String userID = parser.getAttributeValue("", "userID");
+ Date date = null;
+ try {
+ date = UTC_FORMAT.parse(parser.getAttributeValue("", "startTime"));
+ }
+ catch (ParseException e) {
+ }
+
+ String email = parser.getAttributeValue("", "email");
+ String username = parser.getAttributeValue("", "username");
+ String question = parser.getAttributeValue("", "question");
+
+ return new ChatInfo(sessionID, userID, date, email, username, question);
+ }
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/packet/AgentStatusRequest.java b/src/org/jivesoftware/smackx/workgroup/packet/AgentStatusRequest.java new file mode 100644 index 0000000..48549d2 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/AgentStatusRequest.java @@ -0,0 +1,163 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * Agent status request packet. This packet is used by agents to request the list of
+ * agents in a workgroup. The response packet contains a list of packets. Presence
+ * packets from individual agents follow.
+ *
+ * @author Matt Tucker
+ */
+public class AgentStatusRequest extends IQ {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "agent-status-request";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";
+
+ private Set<Item> agents;
+
+ public AgentStatusRequest() {
+ agents = new HashSet<Item>();
+ }
+
+ public int getAgentCount() {
+ return agents.size();
+ }
+
+ public Set<Item> getAgents() {
+ return Collections.unmodifiableSet(agents);
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");
+ synchronized (agents) {
+ for (Iterator<Item> i=agents.iterator(); i.hasNext(); ) {
+ Item item = (Item) i.next();
+ buf.append("<agent jid=\"").append(item.getJID()).append("\">");
+ if (item.getName() != null) {
+ buf.append("<name xmlns=\""+ AgentInfo.NAMESPACE + "\">");
+ buf.append(item.getName());
+ buf.append("</name>");
+ }
+ buf.append("</agent>");
+ }
+ }
+ buf.append("</").append(this.getElementName()).append("> ");
+ return buf.toString();
+ }
+
+ public static class Item {
+
+ private String jid;
+ private String type;
+ private String name;
+
+ public Item(String jid, String type, String name) {
+ this.jid = jid;
+ this.type = type;
+ this.name = name;
+ }
+
+ public String getJID() {
+ return jid;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ /**
+ * Packet extension provider for AgentStatusRequest packets.
+ */
+ public static class Provider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ AgentStatusRequest statusRequest = new AgentStatusRequest();
+
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ throw new IllegalStateException("Parser not in proper position, or bad XML.");
+ }
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if ((eventType == XmlPullParser.START_TAG) && ("agent".equals(parser.getName()))) {
+ statusRequest.agents.add(parseAgent(parser));
+ }
+ else if (eventType == XmlPullParser.END_TAG &&
+ "agent-status-request".equals(parser.getName()))
+ {
+ done = true;
+ }
+ }
+ return statusRequest;
+ }
+
+ private Item parseAgent(XmlPullParser parser) throws Exception {
+
+ boolean done = false;
+ String jid = parser.getAttributeValue("", "jid");
+ String type = parser.getAttributeValue("", "type");
+ String name = null;
+ while (!done) {
+ int eventType = parser.next();
+ if ((eventType == XmlPullParser.START_TAG) && ("name".equals(parser.getName()))) {
+ name = parser.nextText();
+ }
+ else if (eventType == XmlPullParser.END_TAG &&
+ "agent".equals(parser.getName()))
+ {
+ done = true;
+ }
+ }
+ return new Item(jid, type, name);
+ }
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/packet/AgentWorkgroups.java b/src/org/jivesoftware/smackx/workgroup/packet/AgentWorkgroups.java new file mode 100644 index 0000000..292a640 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/AgentWorkgroups.java @@ -0,0 +1,129 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Represents a request for getting the jid of the workgroups where an agent can work or could
+ * represent the result of such request which will contain the list of workgroups JIDs where the
+ * agent can work.
+ *
+ * @author Gaston Dombiak
+ */
+public class AgentWorkgroups extends IQ {
+
+ private String agentJID;
+ private List<String> workgroups;
+
+ /**
+ * Creates an AgentWorkgroups request for the given agent. This IQ will be sent and an answer
+ * will be received with the jid of the workgroups where the agent can work.
+ *
+ * @param agentJID the id of the agent to get his workgroups.
+ */
+ public AgentWorkgroups(String agentJID) {
+ this.agentJID = agentJID;
+ this.workgroups = new ArrayList<String>();
+ }
+
+ /**
+ * Creates an AgentWorkgroups which will contain the JIDs of the workgroups where an agent can
+ * work.
+ *
+ * @param agentJID the id of the agent that can work in the list of workgroups.
+ * @param workgroups the list of workgroup JIDs where the agent can work.
+ */
+ public AgentWorkgroups(String agentJID, List<String> workgroups) {
+ this.agentJID = agentJID;
+ this.workgroups = workgroups;
+ }
+
+ public String getAgentJID() {
+ return agentJID;
+ }
+
+ /**
+ * Returns a list of workgroup JIDs where the agent can work.
+ *
+ * @return a list of workgroup JIDs where the agent can work.
+ */
+ public List<String> getWorkgroups() {
+ return Collections.unmodifiableList(workgroups);
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<workgroups xmlns=\"http://jabber.org/protocol/workgroup\" jid=\"")
+ .append(agentJID)
+ .append("\">");
+
+ for (Iterator<String> it=workgroups.iterator(); it.hasNext();) {
+ String workgroupJID = it.next();
+ buf.append("<workgroup jid=\"" + workgroupJID + "\"/>");
+ }
+
+ buf.append("</workgroups>");
+
+ return buf.toString();
+ }
+
+ /**
+ * An IQProvider for AgentWorkgroups packets.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Provider implements IQProvider {
+
+ public Provider() {
+ super();
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ String agentJID = parser.getAttributeValue("", "jid");
+ List<String> workgroups = new ArrayList<String>();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("workgroup")) {
+ workgroups.add(parser.getAttributeValue("", "jid"));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("workgroups")) {
+ done = true;
+ }
+ }
+ }
+
+ return new AgentWorkgroups(agentJID, workgroups);
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/DepartQueuePacket.java b/src/org/jivesoftware/smackx/workgroup/packet/DepartQueuePacket.java new file mode 100644 index 0000000..620291c --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/DepartQueuePacket.java @@ -0,0 +1,75 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+
+/**
+ * A IQ packet used to depart a workgroup queue. There are two cases for issuing a depart
+ * queue request:<ul>
+ * <li>The user wants to leave the queue. In this case, an instance of this class
+ * should be created without passing in a user address.
+ * <li>An administrator or the server removes wants to remove a user from the queue.
+ * In that case, the address of the user to remove from the queue should be
+ * used to create an instance of this class.</ul>
+ *
+ * @author loki der quaeler
+ */
+public class DepartQueuePacket extends IQ {
+
+ private String user;
+
+ /**
+ * Creates a depart queue request packet to the specified workgroup.
+ *
+ * @param workgroup the workgroup to depart.
+ */
+ public DepartQueuePacket(String workgroup) {
+ this(workgroup, null);
+ }
+
+ /**
+ * Creates a depart queue request to the specified workgroup and for the
+ * specified user.
+ *
+ * @param workgroup the workgroup to depart.
+ * @param user the user to make depart from the queue.
+ */
+ public DepartQueuePacket(String workgroup, String user) {
+ this.user = user;
+
+ setTo(workgroup);
+ setType(IQ.Type.SET);
+ setFrom(user);
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder("<depart-queue xmlns=\"http://jabber.org/protocol/workgroup\"");
+
+ if (this.user != null) {
+ buf.append("><jid>").append(this.user).append("</jid></depart-queue>");
+ }
+ else {
+ buf.append("/>");
+ }
+
+ return buf.toString();
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/packet/MetaDataProvider.java b/src/org/jivesoftware/smackx/workgroup/packet/MetaDataProvider.java new file mode 100644 index 0000000..af76986 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/MetaDataProvider.java @@ -0,0 +1,49 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smackx.workgroup.MetaData;
+import org.jivesoftware.smackx.workgroup.util.MetaDataUtils;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * This provider parses meta data if it's not contained already in a larger extension provider.
+ *
+ * @author loki der quaeler
+ */
+public class MetaDataProvider implements PacketExtensionProvider {
+
+ /**
+ * PacketExtensionProvider implementation
+ */
+ public PacketExtension parseExtension (XmlPullParser parser)
+ throws Exception {
+ Map<String, List<String>> metaData = MetaDataUtils.parseMetaData(parser);
+
+ return new MetaData(metaData);
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/packet/MonitorPacket.java b/src/org/jivesoftware/smackx/workgroup/packet/MonitorPacket.java new file mode 100644 index 0000000..0ceecae --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/MonitorPacket.java @@ -0,0 +1,113 @@ +/** + * 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.smackx.workgroup.packet; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.xmlpull.v1.XmlPullParser; + +public class MonitorPacket extends IQ { + + private String sessionID; + + private boolean isMonitor; + + public boolean isMonitor() { + return isMonitor; + } + + public void setMonitor(boolean monitor) { + isMonitor = monitor; + } + + public String getSessionID() { + return sessionID; + } + + public void setSessionID(String sessionID) { + this.sessionID = sessionID; + } + + /** + * Element name of the packet extension. + */ + public static final String ELEMENT_NAME = "monitor"; + + /** + * Namespace of the packet extension. + */ + public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup"; + + public String getElementName() { + return ELEMENT_NAME; + } + + public String getNamespace() { + return NAMESPACE; + } + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + + buf.append("<").append(ELEMENT_NAME).append(" xmlns="); + buf.append('"'); + buf.append(NAMESPACE); + buf.append('"'); + buf.append(">"); + if (sessionID != null) { + buf.append("<makeOwner sessionID=\""+sessionID+"\"></makeOwner>"); + } + buf.append("</").append(ELEMENT_NAME).append("> "); + return buf.toString(); + } + + + /** + * Packet extension provider for Monitor Packets. + */ + public static class InternalProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + if (parser.getEventType() != XmlPullParser.START_TAG) { + throw new IllegalStateException("Parser not in proper position, or bad XML."); + } + + MonitorPacket packet = new MonitorPacket(); + + boolean done = false; + + + while (!done) { + int eventType = parser.next(); + if ((eventType == XmlPullParser.START_TAG) && ("isMonitor".equals(parser.getName()))) { + String value = parser.nextText(); + if ("false".equalsIgnoreCase(value)) { + packet.setMonitor(false); + } + else { + packet.setMonitor(true); + } + } + else if (eventType == XmlPullParser.END_TAG && "monitor".equals(parser.getName())) { + done = true; + } + } + + return packet; + } + } +} diff --git a/src/org/jivesoftware/smackx/workgroup/packet/OccupantsInfo.java b/src/org/jivesoftware/smackx/workgroup/packet/OccupantsInfo.java new file mode 100644 index 0000000..0f80866 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/OccupantsInfo.java @@ -0,0 +1,173 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * Packet used for requesting information about occupants of a room or for retrieving information
+ * such information.
+ *
+ * @author Gaston Dombiak
+ */
+public class OccupantsInfo extends IQ {
+
+ private static final SimpleDateFormat UTC_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
+
+ static {
+ UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+0"));
+ }
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "occupants-info";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+ private String roomID;
+ private final Set<OccupantInfo> occupants;
+
+ public OccupantsInfo(String roomID) {
+ this.roomID = roomID;
+ this.occupants = new HashSet<OccupantInfo>();
+ }
+
+ public String getRoomID() {
+ return roomID;
+ }
+
+ public int getOccupantsCount() {
+ return occupants.size();
+ }
+
+ public Set<OccupantInfo> getOccupants() {
+ return Collections.unmodifiableSet(occupants);
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE);
+ buf.append("\" roomID=\"").append(roomID).append("\">");
+ synchronized (occupants) {
+ for (OccupantInfo occupant : occupants) {
+ buf.append("<occupant>");
+ // Add the occupant jid
+ buf.append("<jid>");
+ buf.append(occupant.getJID());
+ buf.append("</jid>");
+ // Add the occupant nickname
+ buf.append("<name>");
+ buf.append(occupant.getNickname());
+ buf.append("</name>");
+ // Add the date when the occupant joined the room
+ buf.append("<joined>");
+ buf.append(UTC_FORMAT.format(occupant.getJoined()));
+ buf.append("</joined>");
+ buf.append("</occupant>");
+ }
+ }
+ buf.append("</").append(ELEMENT_NAME).append("> ");
+ return buf.toString();
+ }
+
+ public static class OccupantInfo {
+
+ private String jid;
+ private String nickname;
+ private Date joined;
+
+ public OccupantInfo(String jid, String nickname, Date joined) {
+ this.jid = jid;
+ this.nickname = nickname;
+ this.joined = joined;
+ }
+
+ public String getJID() {
+ return jid;
+ }
+
+ public String getNickname() {
+ return nickname;
+ }
+
+ public Date getJoined() {
+ return joined;
+ }
+ }
+
+ /**
+ * Packet extension provider for AgentStatusRequest packets.
+ */
+ public static class Provider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ throw new IllegalStateException("Parser not in proper position, or bad XML.");
+ }
+ OccupantsInfo occupantsInfo = new OccupantsInfo(parser.getAttributeValue("", "roomID"));
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if ((eventType == XmlPullParser.START_TAG) &&
+ ("occupant".equals(parser.getName()))) {
+ occupantsInfo.occupants.add(parseOccupantInfo(parser));
+ } else if (eventType == XmlPullParser.END_TAG &&
+ ELEMENT_NAME.equals(parser.getName())) {
+ done = true;
+ }
+ }
+ return occupantsInfo;
+ }
+
+ private OccupantInfo parseOccupantInfo(XmlPullParser parser) throws Exception {
+
+ boolean done = false;
+ String jid = null;
+ String nickname = null;
+ Date joined = null;
+ while (!done) {
+ int eventType = parser.next();
+ if ((eventType == XmlPullParser.START_TAG) && ("jid".equals(parser.getName()))) {
+ jid = parser.nextText();
+ } else if ((eventType == XmlPullParser.START_TAG) &&
+ ("nickname".equals(parser.getName()))) {
+ nickname = parser.nextText();
+ } else if ((eventType == XmlPullParser.START_TAG) &&
+ ("joined".equals(parser.getName()))) {
+ joined = UTC_FORMAT.parse(parser.nextText());
+ } else if (eventType == XmlPullParser.END_TAG &&
+ "occupant".equals(parser.getName())) {
+ done = true;
+ }
+ }
+ return new OccupantInfo(jid, nickname, joined);
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/OfferRequestProvider.java b/src/org/jivesoftware/smackx/workgroup/packet/OfferRequestProvider.java new file mode 100644 index 0000000..8f56b78 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/OfferRequestProvider.java @@ -0,0 +1,211 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smackx.workgroup.MetaData;
+import org.jivesoftware.smackx.workgroup.agent.InvitationRequest;
+import org.jivesoftware.smackx.workgroup.agent.OfferContent;
+import org.jivesoftware.smackx.workgroup.agent.TransferRequest;
+import org.jivesoftware.smackx.workgroup.agent.UserRequest;
+import org.jivesoftware.smackx.workgroup.util.MetaDataUtils;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An IQProvider for agent offer requests.
+ *
+ * @author loki der quaeler
+ */
+public class OfferRequestProvider implements IQProvider {
+
+ public OfferRequestProvider() {
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ int eventType = parser.getEventType();
+ String sessionID = null;
+ int timeout = -1;
+ OfferContent content = null;
+ boolean done = false;
+ Map<String, List<String>> metaData = new HashMap<String, List<String>>();
+
+ if (eventType != XmlPullParser.START_TAG) {
+ // throw exception
+ }
+
+ String userJID = parser.getAttributeValue("", "jid");
+ // Default userID to the JID.
+ String userID = userJID;
+
+ while (!done) {
+ eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG) {
+ String elemName = parser.getName();
+
+ if ("timeout".equals(elemName)) {
+ timeout = Integer.parseInt(parser.nextText());
+ }
+ else if (MetaData.ELEMENT_NAME.equals(elemName)) {
+ metaData = MetaDataUtils.parseMetaData(parser);
+ }
+ else if (SessionID.ELEMENT_NAME.equals(elemName)) {
+ sessionID = parser.getAttributeValue("", "id");
+ }
+ else if (UserID.ELEMENT_NAME.equals(elemName)) {
+ userID = parser.getAttributeValue("", "id");
+ }
+ else if ("user-request".equals(elemName)) {
+ content = UserRequest.getInstance();
+ }
+ else if (RoomInvitation.ELEMENT_NAME.equals(elemName)) {
+ RoomInvitation invitation = (RoomInvitation) PacketParserUtils
+ .parsePacketExtension(RoomInvitation.ELEMENT_NAME, RoomInvitation.NAMESPACE, parser);
+ content = new InvitationRequest(invitation.getInviter(), invitation.getRoom(),
+ invitation.getReason());
+ }
+ else if (RoomTransfer.ELEMENT_NAME.equals(elemName)) {
+ RoomTransfer transfer = (RoomTransfer) PacketParserUtils
+ .parsePacketExtension(RoomTransfer.ELEMENT_NAME, RoomTransfer.NAMESPACE, parser);
+ content = new TransferRequest(transfer.getInviter(), transfer.getRoom(), transfer.getReason());
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if ("offer".equals(parser.getName())) {
+ done = true;
+ }
+ }
+ }
+
+ OfferRequestPacket offerRequest =
+ new OfferRequestPacket(userJID, userID, timeout, metaData, sessionID, content);
+ offerRequest.setType(IQ.Type.SET);
+
+ return offerRequest;
+ }
+
+ public static class OfferRequestPacket extends IQ {
+
+ private int timeout;
+ private String userID;
+ private String userJID;
+ private Map<String, List<String>> metaData;
+ private String sessionID;
+ private OfferContent content;
+
+ public OfferRequestPacket(String userJID, String userID, int timeout, Map<String, List<String>> metaData,
+ String sessionID, OfferContent content)
+ {
+ this.userJID = userJID;
+ this.userID = userID;
+ this.timeout = timeout;
+ this.metaData = metaData;
+ this.sessionID = sessionID;
+ this.content = content;
+ }
+
+ /**
+ * Returns the userID, which is either the same as the userJID or a special
+ * value that the user provided as part of their "join queue" request.
+ *
+ * @return the user ID.
+ */
+ public String getUserID() {
+ return userID;
+ }
+
+ /**
+ * The JID of the user that made the "join queue" request.
+ *
+ * @return the user JID.
+ */
+ public String getUserJID() {
+ return userJID;
+ }
+
+ /**
+ * Returns the session ID associated with the request and ensuing chat. If the offer
+ * does not contain a session ID, <tt>null</tt> will be returned.
+ *
+ * @return the session id associated with the request.
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ /**
+ * Returns the number of seconds the agent has to accept the offer before
+ * it times out.
+ *
+ * @return the offer timeout (in seconds).
+ */
+ public int getTimeout() {
+ return this.timeout;
+ }
+
+ public OfferContent getContent() {
+ return content;
+ }
+
+ /**
+ * Returns any meta-data associated with the offer.
+ *
+ * @return meta-data associated with the offer.
+ */
+ public Map<String, List<String>> getMetaData() {
+ return this.metaData;
+ }
+
+ public String getChildElementXML () {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<offer xmlns=\"http://jabber.org/protocol/workgroup\" jid=\"").append(userJID).append("\">");
+ buf.append("<timeout>").append(timeout).append("</timeout>");
+
+ if (sessionID != null) {
+ buf.append('<').append(SessionID.ELEMENT_NAME);
+ buf.append(" session=\"");
+ buf.append(getSessionID()).append("\" xmlns=\"");
+ buf.append(SessionID.NAMESPACE).append("\"/>");
+ }
+
+ if (metaData != null) {
+ buf.append(MetaDataUtils.serializeMetaData(metaData));
+ }
+
+ if (userID != null) {
+ buf.append('<').append(UserID.ELEMENT_NAME);
+ buf.append(" id=\"");
+ buf.append(userID).append("\" xmlns=\"");
+ buf.append(UserID.NAMESPACE).append("\"/>");
+ }
+
+ buf.append("</offer>");
+
+ return buf.toString();
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/OfferRevokeProvider.java b/src/org/jivesoftware/smackx/workgroup/packet/OfferRevokeProvider.java new file mode 100644 index 0000000..202824c --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/OfferRevokeProvider.java @@ -0,0 +1,112 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * An IQProvider class which has savvy about the offer-revoke tag.<br>
+ *
+ * @author loki der quaeler
+ */
+public class OfferRevokeProvider implements IQProvider {
+
+ public IQ parseIQ (XmlPullParser parser) throws Exception {
+ // The parser will be positioned on the opening IQ tag, so get the JID attribute.
+ String userJID = parser.getAttributeValue("", "jid");
+ // Default the userID to the JID.
+ String userID = userJID;
+ String reason = null;
+ String sessionID = null;
+ boolean done = false;
+
+ while (!done) {
+ int eventType = parser.next();
+
+ if ((eventType == XmlPullParser.START_TAG) && parser.getName().equals("reason")) {
+ reason = parser.nextText();
+ }
+ else if ((eventType == XmlPullParser.START_TAG)
+ && parser.getName().equals(SessionID.ELEMENT_NAME)) {
+ sessionID = parser.getAttributeValue("", "id");
+ }
+ else if ((eventType == XmlPullParser.START_TAG)
+ && parser.getName().equals(UserID.ELEMENT_NAME)) {
+ userID = parser.getAttributeValue("", "id");
+ }
+ else if ((eventType == XmlPullParser.END_TAG) && parser.getName().equals(
+ "offer-revoke"))
+ {
+ done = true;
+ }
+ }
+
+ return new OfferRevokePacket(userJID, userID, reason, sessionID);
+ }
+
+ public class OfferRevokePacket extends IQ {
+
+ private String userJID;
+ private String userID;
+ private String sessionID;
+ private String reason;
+
+ public OfferRevokePacket (String userJID, String userID, String cause, String sessionID) {
+ this.userJID = userJID;
+ this.userID = userID;
+ this.reason = cause;
+ this.sessionID = sessionID;
+ }
+
+ public String getUserJID() {
+ return userJID;
+ }
+
+ public String getUserID() {
+ return this.userID;
+ }
+
+ public String getReason() {
+ return this.reason;
+ }
+
+ public String getSessionID() {
+ return this.sessionID;
+ }
+
+ public String getChildElementXML () {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<offer-revoke xmlns=\"http://jabber.org/protocol/workgroup\" jid=\"").append(userID).append("\">");
+ if (reason != null) {
+ buf.append("<reason>").append(reason).append("</reason>");
+ }
+ if (sessionID != null) {
+ buf.append(new SessionID(sessionID).toXML());
+ }
+ if (userID != null) {
+ buf.append(new UserID(userID).toXML());
+ }
+ buf.append("</offer-revoke>");
+ return buf.toString();
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/QueueDetails.java b/src/org/jivesoftware/smackx/workgroup/packet/QueueDetails.java new file mode 100644 index 0000000..ef11a78 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/QueueDetails.java @@ -0,0 +1,199 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smackx.workgroup.QueueUser;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * Queue details packet extension, which contains details about the users
+ * currently in a queue.
+ */
+public class QueueDetails implements PacketExtension {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "notify-queue-details";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";
+
+ private static final String DATE_FORMAT = "yyyyMMdd'T'HH:mm:ss";
+
+ private SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
+ /**
+ * The list of users in the queue.
+ */
+ private Set<QueueUser> users;
+
+ /**
+ * Creates a new QueueDetails packet
+ */
+ private QueueDetails() {
+ users = new HashSet<QueueUser>();
+ }
+
+ /**
+ * Returns the number of users currently in the queue that are waiting to
+ * be routed to an agent.
+ *
+ * @return the number of users in the queue.
+ */
+ public int getUserCount() {
+ return users.size();
+ }
+
+ /**
+ * Returns the set of users in the queue that are waiting to
+ * be routed to an agent (as QueueUser objects).
+ *
+ * @return a Set for the users waiting in a queue.
+ */
+ public Set<QueueUser> getUsers() {
+ synchronized (users) {
+ return users;
+ }
+ }
+
+ /**
+ * Adds a user to the packet.
+ *
+ * @param user the user.
+ */
+ private void addUser(QueueUser user) {
+ synchronized (users) {
+ users.add(user);
+ }
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");
+
+ synchronized (users) {
+ for (Iterator<QueueUser> i=users.iterator(); i.hasNext(); ) {
+ QueueUser user = (QueueUser)i.next();
+ int position = user.getQueuePosition();
+ int timeRemaining = user.getEstimatedRemainingTime();
+ Date timestamp = user.getQueueJoinTimestamp();
+
+ buf.append("<user jid=\"").append(user.getUserID()).append("\">");
+
+ if (position != -1) {
+ buf.append("<position>").append(position).append("</position>");
+ }
+
+ if (timeRemaining != -1) {
+ buf.append("<time>").append(timeRemaining).append("</time>");
+ }
+
+ if (timestamp != null) {
+ buf.append("<join-time>");
+ buf.append(dateFormat.format(timestamp));
+ buf.append("</join-time>");
+ }
+
+ buf.append("</user>");
+ }
+ }
+ buf.append("</").append(ELEMENT_NAME).append(">");
+ return buf.toString();
+ }
+
+ /**
+ * Provider class for QueueDetails packet extensions.
+ */
+ public static class Provider implements PacketExtensionProvider {
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+
+ SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
+ QueueDetails queueDetails = new QueueDetails();
+
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.END_TAG &&
+ "notify-queue-details".equals(parser.getName()))
+ {
+ eventType = parser.next();
+ while ((eventType == XmlPullParser.START_TAG) && "user".equals(parser.getName())) {
+ String uid = null;
+ int position = -1;
+ int time = -1;
+ Date joinTime = null;
+
+ uid = parser.getAttributeValue("", "jid");
+
+ if (uid == null) {
+ // throw exception
+ }
+
+ eventType = parser.next();
+ while ((eventType != XmlPullParser.END_TAG)
+ || (! "user".equals(parser.getName())))
+ {
+ if ("position".equals(parser.getName())) {
+ position = Integer.parseInt(parser.nextText());
+ }
+ else if ("time".equals(parser.getName())) {
+ time = Integer.parseInt(parser.nextText());
+ }
+ else if ("join-time".equals(parser.getName())) {
+ joinTime = dateFormat.parse(parser.nextText());
+ }
+ else if( parser.getName().equals( "waitTime" ) ) {
+ Date wait = dateFormat.parse(parser.nextText());
+ System.out.println( wait );
+ }
+
+ eventType = parser.next();
+
+ if (eventType != XmlPullParser.END_TAG) {
+ // throw exception
+ }
+ }
+
+ queueDetails.addUser(new QueueUser(uid, position, time, joinTime));
+
+ eventType = parser.next();
+ }
+ }
+ return queueDetails;
+ }
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/packet/QueueOverview.java b/src/org/jivesoftware/smackx/workgroup/packet/QueueOverview.java new file mode 100644 index 0000000..a559579 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/QueueOverview.java @@ -0,0 +1,160 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smackx.workgroup.agent.WorkgroupQueue;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class QueueOverview implements PacketExtension {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static String ELEMENT_NAME = "notify-queue";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static String NAMESPACE = "http://jabber.org/protocol/workgroup";
+
+ private static final String DATE_FORMAT = "yyyyMMdd'T'HH:mm:ss";
+ private SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
+
+ private int averageWaitTime;
+ private Date oldestEntry;
+ private int userCount;
+ private WorkgroupQueue.Status status;
+
+ QueueOverview() {
+ this.averageWaitTime = -1;
+ this.oldestEntry = null;
+ this.userCount = -1;
+ this.status = null;
+ }
+
+ void setAverageWaitTime(int averageWaitTime) {
+ this.averageWaitTime = averageWaitTime;
+ }
+
+ public int getAverageWaitTime () {
+ return averageWaitTime;
+ }
+
+ void setOldestEntry(Date oldestEntry) {
+ this.oldestEntry = oldestEntry;
+ }
+
+ public Date getOldestEntry() {
+ return oldestEntry;
+ }
+
+ void setUserCount(int userCount) {
+ this.userCount = userCount;
+ }
+
+ public int getUserCount() {
+ return userCount;
+ }
+
+ public WorkgroupQueue.Status getStatus() {
+ return status;
+ }
+
+ void setStatus(WorkgroupQueue.Status status) {
+ this.status = status;
+ }
+
+ public String getElementName () {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace () {
+ return NAMESPACE;
+ }
+
+ public String toXML () {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");
+
+ if (userCount != -1) {
+ buf.append("<count>").append(userCount).append("</count>");
+ }
+ if (oldestEntry != null) {
+ buf.append("<oldest>").append(dateFormat.format(oldestEntry)).append("</oldest>");
+ }
+ if (averageWaitTime != -1) {
+ buf.append("<time>").append(averageWaitTime).append("</time>");
+ }
+ if (status != null) {
+ buf.append("<status>").append(status).append("</status>");
+ }
+ buf.append("</").append(ELEMENT_NAME).append(">");
+
+ return buf.toString();
+ }
+
+ public static class Provider implements PacketExtensionProvider {
+
+ public PacketExtension parseExtension (XmlPullParser parser) throws Exception {
+ int eventType = parser.getEventType();
+ QueueOverview queueOverview = new QueueOverview();
+ SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
+
+ if (eventType != XmlPullParser.START_TAG) {
+ // throw exception
+ }
+
+ eventType = parser.next();
+ while ((eventType != XmlPullParser.END_TAG)
+ || (!ELEMENT_NAME.equals(parser.getName())))
+ {
+ if ("count".equals(parser.getName())) {
+ queueOverview.setUserCount(Integer.parseInt(parser.nextText()));
+ }
+ else if ("time".equals(parser.getName())) {
+ queueOverview.setAverageWaitTime(Integer.parseInt(parser.nextText()));
+ }
+ else if ("oldest".equals(parser.getName())) {
+ queueOverview.setOldestEntry((dateFormat.parse(parser.nextText())));
+ }
+ else if ("status".equals(parser.getName())) {
+ queueOverview.setStatus(WorkgroupQueue.Status.fromString(parser.nextText()));
+ }
+
+ eventType = parser.next();
+
+ if (eventType != XmlPullParser.END_TAG) {
+ // throw exception
+ }
+ }
+
+ if (eventType != XmlPullParser.END_TAG) {
+ // throw exception
+ }
+
+ return queueOverview;
+ }
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/packet/QueueUpdate.java b/src/org/jivesoftware/smackx/workgroup/packet/QueueUpdate.java new file mode 100644 index 0000000..c326a57 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/QueueUpdate.java @@ -0,0 +1,122 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * An IQ packet that encapsulates both types of workgroup queue
+ * status notifications -- position updates, and estimated time
+ * left in the queue updates.
+ */
+public class QueueUpdate implements PacketExtension {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "queue-status";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";
+
+ private int position;
+ private int remainingTime;
+
+ public QueueUpdate(int position, int remainingTime) {
+ this.position = position;
+ this.remainingTime = remainingTime;
+ }
+
+ /**
+ * Returns the user's position in the workgroup queue, or -1 if the
+ * value isn't set on this packet.
+ *
+ * @return the position in the workgroup queue.
+ */
+ public int getPosition() {
+ return this.position;
+ }
+
+ /**
+ * Returns the user's estimated time left in the workgroup queue, or
+ * -1 if the value isn't set on this packet.
+ *
+ * @return the estimated time left in the workgroup queue.
+ */
+ public int getRemaingTime() {
+ return remainingTime;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("<queue-status xmlns=\"http://jabber.org/protocol/workgroup\">");
+ if (position != -1) {
+ buf.append("<position>").append(position).append("</position>");
+ }
+ if (remainingTime != -1) {
+ buf.append("<time>").append(remainingTime).append("</time>");
+ }
+ buf.append("</queue-status>");
+ return buf.toString();
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public static class Provider implements PacketExtensionProvider {
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ boolean done = false;
+ int position = -1;
+ int timeRemaining = -1;
+ while (!done) {
+ parser.next();
+ String elementName = parser.getName();
+ if (parser.getEventType() == XmlPullParser.START_TAG && "position".equals(elementName)) {
+ try {
+ position = Integer.parseInt(parser.nextText());
+ }
+ catch (NumberFormatException nfe) {
+ }
+ }
+ else if (parser.getEventType() == XmlPullParser.START_TAG && "time".equals(elementName)) {
+ try {
+ timeRemaining = Integer.parseInt(parser.nextText());
+ }
+ catch (NumberFormatException nfe) {
+ }
+ }
+ else if (parser.getEventType() == XmlPullParser.END_TAG && "queue-status".equals(elementName)) {
+ done = true;
+ }
+ }
+ return new QueueUpdate(position, timeRemaining);
+ }
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/packet/RoomInvitation.java b/src/org/jivesoftware/smackx/workgroup/packet/RoomInvitation.java new file mode 100644 index 0000000..34555de --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/RoomInvitation.java @@ -0,0 +1,177 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Packet extension for {@link org.jivesoftware.smackx.workgroup.agent.InvitationRequest}.
+ *
+ * @author Gaston Dombiak
+ */
+public class RoomInvitation implements PacketExtension {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "invite";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";
+
+ /**
+ * Type of entity being invited to a groupchat support session.
+ */
+ private Type type;
+ /**
+ * JID of the entity being invited. The entity could be another agent, user , a queue or a workgroup. In
+ * the case of a queue or a workgroup the server will select the best agent to invite.
+ */
+ private String invitee;
+ /**
+ * Full JID of the user that sent the invitation.
+ */
+ private String inviter;
+ /**
+ * ID of the session that originated the initial user request.
+ */
+ private String sessionID;
+ /**
+ * JID of the room to join if offer is accepted.
+ */
+ private String room;
+ /**
+ * Text provided by the inviter explaining the reason why the invitee is invited.
+ */
+ private String reason;
+
+ public RoomInvitation(Type type, String invitee, String sessionID, String reason) {
+ this.type = type;
+ this.invitee = invitee;
+ this.sessionID = sessionID;
+ this.reason = reason;
+ }
+
+ private RoomInvitation() {
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String getInviter() {
+ return inviter;
+ }
+
+ public String getRoom() {
+ return room;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE);
+ buf.append("\" type=\"").append(type).append("\">");
+ buf.append("<session xmlns=\"http://jivesoftware.com/protocol/workgroup\" id=\"").append(sessionID).append("\"></session>");
+ if (invitee != null) {
+ buf.append("<invitee>").append(invitee).append("</invitee>");
+ }
+ if (inviter != null) {
+ buf.append("<inviter>").append(inviter).append("</inviter>");
+ }
+ if (reason != null) {
+ buf.append("<reason>").append(reason).append("</reason>");
+ }
+ // Add packet extensions, if any are defined.
+ buf.append("</").append(ELEMENT_NAME).append("> ");
+
+ return buf.toString();
+ }
+
+ /**
+ * Type of entity being invited to a groupchat support session.
+ */
+ public static enum Type {
+ /**
+ * A user is being invited to a groupchat support session. The user could be another agent
+ * or just a regular XMPP user.
+ */
+ user,
+ /**
+ * Some agent of the specified queue will be invited to the groupchat support session.
+ */
+ queue,
+ /**
+ * Some agent of the specified workgroup will be invited to the groupchat support session.
+ */
+ workgroup
+ }
+
+ public static class Provider implements PacketExtensionProvider {
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ final RoomInvitation invitation = new RoomInvitation();
+ invitation.type = Type.valueOf(parser.getAttributeValue("", "type"));
+
+ boolean done = false;
+ while (!done) {
+ parser.next();
+ String elementName = parser.getName();
+ if (parser.getEventType() == XmlPullParser.START_TAG) {
+ if ("session".equals(elementName)) {
+ invitation.sessionID = parser.getAttributeValue("", "id");
+ }
+ else if ("invitee".equals(elementName)) {
+ invitation.invitee = parser.nextText();
+ }
+ else if ("inviter".equals(elementName)) {
+ invitation.inviter = parser.nextText();
+ }
+ else if ("reason".equals(elementName)) {
+ invitation.reason = parser.nextText();
+ }
+ else if ("room".equals(elementName)) {
+ invitation.room = parser.nextText();
+ }
+ }
+ else if (parser.getEventType() == XmlPullParser.END_TAG && ELEMENT_NAME.equals(elementName)) {
+ done = true;
+ }
+ }
+ return invitation;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/RoomTransfer.java b/src/org/jivesoftware/smackx/workgroup/packet/RoomTransfer.java new file mode 100644 index 0000000..d1e83e2 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/RoomTransfer.java @@ -0,0 +1,177 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Packet extension for {@link org.jivesoftware.smackx.workgroup.agent.TransferRequest}.
+ *
+ * @author Gaston Dombiak
+ */
+public class RoomTransfer implements PacketExtension {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "transfer";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";
+
+ /**
+ * Type of entity being invited to a groupchat support session.
+ */
+ private RoomTransfer.Type type;
+ /**
+ * JID of the entity being invited. The entity could be another agent, user , a queue or a workgroup. In
+ * the case of a queue or a workgroup the server will select the best agent to invite.
+ */
+ private String invitee;
+ /**
+ * Full JID of the user that sent the invitation.
+ */
+ private String inviter;
+ /**
+ * ID of the session that originated the initial user request.
+ */
+ private String sessionID;
+ /**
+ * JID of the room to join if offer is accepted.
+ */
+ private String room;
+ /**
+ * Text provided by the inviter explaining the reason why the invitee is invited.
+ */
+ private String reason;
+
+ public RoomTransfer(RoomTransfer.Type type, String invitee, String sessionID, String reason) {
+ this.type = type;
+ this.invitee = invitee;
+ this.sessionID = sessionID;
+ this.reason = reason;
+ }
+
+ private RoomTransfer() {
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String getInviter() {
+ return inviter;
+ }
+
+ public String getRoom() {
+ return room;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE);
+ buf.append("\" type=\"").append(type).append("\">");
+ buf.append("<session xmlns=\"http://jivesoftware.com/protocol/workgroup\" id=\"").append(sessionID).append("\"></session>");
+ if (invitee != null) {
+ buf.append("<invitee>").append(invitee).append("</invitee>");
+ }
+ if (inviter != null) {
+ buf.append("<inviter>").append(inviter).append("</inviter>");
+ }
+ if (reason != null) {
+ buf.append("<reason>").append(reason).append("</reason>");
+ }
+ // Add packet extensions, if any are defined.
+ buf.append("</").append(ELEMENT_NAME).append("> ");
+
+ return buf.toString();
+ }
+
+ /**
+ * Type of entity being invited to a groupchat support session.
+ */
+ public static enum Type {
+ /**
+ * A user is being invited to a groupchat support session. The user could be another agent
+ * or just a regular XMPP user.
+ */
+ user,
+ /**
+ * Some agent of the specified queue will be invited to the groupchat support session.
+ */
+ queue,
+ /**
+ * Some agent of the specified workgroup will be invited to the groupchat support session.
+ */
+ workgroup
+ }
+
+ public static class Provider implements PacketExtensionProvider {
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ final RoomTransfer invitation = new RoomTransfer();
+ invitation.type = RoomTransfer.Type.valueOf(parser.getAttributeValue("", "type"));
+
+ boolean done = false;
+ while (!done) {
+ parser.next();
+ String elementName = parser.getName();
+ if (parser.getEventType() == XmlPullParser.START_TAG) {
+ if ("session".equals(elementName)) {
+ invitation.sessionID = parser.getAttributeValue("", "id");
+ }
+ else if ("invitee".equals(elementName)) {
+ invitation.invitee = parser.nextText();
+ }
+ else if ("inviter".equals(elementName)) {
+ invitation.inviter = parser.nextText();
+ }
+ else if ("reason".equals(elementName)) {
+ invitation.reason = parser.nextText();
+ }
+ else if ("room".equals(elementName)) {
+ invitation.room = parser.nextText();
+ }
+ }
+ else if (parser.getEventType() == XmlPullParser.END_TAG && ELEMENT_NAME.equals(elementName)) {
+ done = true;
+ }
+ }
+ return invitation;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/SessionID.java b/src/org/jivesoftware/smackx/workgroup/packet/SessionID.java new file mode 100644 index 0000000..bfd7cfd --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/SessionID.java @@ -0,0 +1,77 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+public class SessionID implements PacketExtension {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "session";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+ private String sessionID;
+
+ public SessionID(String sessionID) {
+ this.sessionID = sessionID;
+ }
+
+ public String getSessionID() {
+ return this.sessionID;
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\" ");
+ buf.append("id=\"").append(this.getSessionID());
+ buf.append("\"/>");
+
+ return buf.toString();
+ }
+
+ public static class Provider implements PacketExtensionProvider {
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ String sessionID = parser.getAttributeValue("", "id");
+
+ // Advance to end of extension.
+ parser.next();
+
+ return new SessionID(sessionID);
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/Transcript.java b/src/org/jivesoftware/smackx/workgroup/packet/Transcript.java new file mode 100644 index 0000000..7f8f29e --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/Transcript.java @@ -0,0 +1,98 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Represents the conversation transcript that occured in a group chat room between an Agent
+ * and a user that requested assistance. The transcript contains all the Messages that were sent
+ * to the room as well as the sent presences.
+ *
+ * @author Gaston Dombiak
+ */
+public class Transcript extends IQ {
+ private String sessionID;
+ private List<Packet> packets;
+
+ /**
+ * Creates a transcript request for the given sessionID.
+ *
+ * @param sessionID the id of the session to get the conversation transcript.
+ */
+ public Transcript(String sessionID) {
+ this.sessionID = sessionID;
+ this.packets = new ArrayList<Packet>();
+ }
+
+ /**
+ * Creates a new transcript for the given sessionID and list of packets. The list of packets
+ * may include Messages and/or Presences.
+ *
+ * @param sessionID the id of the session that generated this conversation transcript.
+ * @param packets the list of messages and presences send to the room.
+ */
+ public Transcript(String sessionID, List<Packet> packets) {
+ this.sessionID = sessionID;
+ this.packets = packets;
+ }
+
+ /**
+ * Returns id of the session that generated this conversation transcript. The sessionID is a
+ * value generated by the server when a new request is received.
+ *
+ * @return id of the session that generated this conversation transcript.
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ /**
+ * Returns the list of Messages and Presences that were sent to the room.
+ *
+ * @return the list of Messages and Presences that were sent to the room.
+ */
+ public List<Packet> getPackets() {
+ return Collections.unmodifiableList(packets);
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<transcript xmlns=\"http://jivesoftware.com/protocol/workgroup\" sessionID=\"")
+ .append(sessionID)
+ .append("\">");
+
+ for (Iterator<Packet> it=packets.iterator(); it.hasNext();) {
+ Packet packet = it.next();
+ buf.append(packet.toXML());
+ }
+
+ buf.append("</transcript>");
+
+ return buf.toString();
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/TranscriptProvider.java b/src/org/jivesoftware/smackx/workgroup/packet/TranscriptProvider.java new file mode 100644 index 0000000..791b06e --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/TranscriptProvider.java @@ -0,0 +1,66 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An IQProvider for transcripts.
+ *
+ * @author Gaston Dombiak
+ */
+public class TranscriptProvider implements IQProvider {
+
+ public TranscriptProvider() {
+ super();
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ String sessionID = parser.getAttributeValue("", "sessionID");
+ List<Packet> packets = new ArrayList<Packet>();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("message")) {
+ packets.add(PacketParserUtils.parseMessage(parser));
+ }
+ else if (parser.getName().equals("presence")) {
+ packets.add(PacketParserUtils.parsePresence(parser));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("transcript")) {
+ done = true;
+ }
+ }
+ }
+
+ return new Transcript(sessionID, packets);
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/TranscriptSearch.java b/src/org/jivesoftware/smackx/workgroup/packet/TranscriptSearch.java new file mode 100644 index 0000000..72693c4 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/TranscriptSearch.java @@ -0,0 +1,87 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * IQ packet for retrieving the transcript search form, submiting the completed search form
+ * or retrieving the answer of a transcript search.
+ *
+ * @author Gaston Dombiak
+ */
+public class TranscriptSearch extends IQ {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "transcript-search";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");
+ // Add packet extensions, if any are defined.
+ buf.append(getExtensionsXML());
+ buf.append("</").append(ELEMENT_NAME).append("> ");
+
+ return buf.toString();
+ }
+
+ /**
+ * An IQProvider for TranscriptSearch packets.
+ *
+ * @author Gaston Dombiak
+ */
+ public static class Provider implements IQProvider {
+
+ public Provider() {
+ super();
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ TranscriptSearch answer = new TranscriptSearch();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ // Parse the packet extension
+ answer.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(), parser.getNamespace(), parser));
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(ELEMENT_NAME)) {
+ done = true;
+ }
+ }
+ }
+
+ return answer;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/Transcripts.java b/src/org/jivesoftware/smackx/workgroup/packet/Transcripts.java new file mode 100644 index 0000000..66ddaad --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/Transcripts.java @@ -0,0 +1,247 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * Represents a list of conversation transcripts that a user had in all his history. Each
+ * transcript summary includes the sessionID which may be used for getting more detailed
+ * information about the conversation. {@link org.jivesoftware.smackx.workgroup.packet.Transcript}
+ *
+ * @author Gaston Dombiak
+ */
+public class Transcripts extends IQ {
+
+ private static final SimpleDateFormat UTC_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
+ static {
+ UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+0"));
+ }
+
+ private String userID;
+ private List<Transcripts.TranscriptSummary> summaries;
+
+
+ /**
+ * Creates a transcripts request for the given userID.
+ *
+ * @param userID the id of the user to get his conversations transcripts.
+ */
+ public Transcripts(String userID) {
+ this.userID = userID;
+ this.summaries = new ArrayList<Transcripts.TranscriptSummary>();
+ }
+
+ /**
+ * Creates a Transcripts which will contain the transcript summaries of the given user.
+ *
+ * @param userID the id of the user. Could be a real JID or a unique String that identifies
+ * anonymous users.
+ * @param summaries the list of TranscriptSummaries.
+ */
+ public Transcripts(String userID, List<Transcripts.TranscriptSummary> summaries) {
+ this.userID = userID;
+ this.summaries = summaries;
+ }
+
+ /**
+ * Returns the id of the user that was involved in the conversations. The userID could be a
+ * real JID if the connected user was not anonymous. Otherwise, the userID will be a String
+ * that was provided by the anonymous user as a way to idenitify the user across many user
+ * sessions.
+ *
+ * @return the id of the user that was involved in the conversations.
+ */
+ public String getUserID() {
+ return userID;
+ }
+
+ /**
+ * Returns a list of TranscriptSummary. A TranscriptSummary does not contain the conversation
+ * transcript but some summary information like the sessionID and the time when the
+ * conversation started and finished. Once you have the sessionID it is possible to get the
+ * full conversation transcript.
+ *
+ * @return a list of TranscriptSummary.
+ */
+ public List<Transcripts.TranscriptSummary> getSummaries() {
+ return Collections.unmodifiableList(summaries);
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<transcripts xmlns=\"http://jivesoftware.com/protocol/workgroup\" userID=\"")
+ .append(userID)
+ .append("\">");
+
+ for (TranscriptSummary transcriptSummary : summaries) {
+ buf.append(transcriptSummary.toXML());
+ }
+
+ buf.append("</transcripts>");
+
+ return buf.toString();
+ }
+
+ /**
+ * A TranscriptSummary contains some information about a conversation such as the ID of the
+ * session or the date when the conversation started and finished. You will need to use the
+ * sessionID to get the full conversation transcript.
+ */
+ public static class TranscriptSummary {
+ private String sessionID;
+ private Date joinTime;
+ private Date leftTime;
+ private List<AgentDetail> agentDetails;
+
+ public TranscriptSummary(String sessionID, Date joinTime, Date leftTime, List<AgentDetail> agentDetails) {
+ this.sessionID = sessionID;
+ this.joinTime = joinTime;
+ this.leftTime = leftTime;
+ this.agentDetails = agentDetails;
+ }
+
+ /**
+ * Returns the ID of the session that is related to this conversation transcript. The
+ * sessionID could be used for getting the full conversation transcript.
+ *
+ * @return the ID of the session that is related to this conversation transcript.
+ */
+ public String getSessionID() {
+ return sessionID;
+ }
+
+ /**
+ * Returns the Date when the conversation started.
+ *
+ * @return the Date when the conversation started.
+ */
+ public Date getJoinTime() {
+ return joinTime;
+ }
+
+ /**
+ * Returns the Date when the conversation finished.
+ *
+ * @return the Date when the conversation finished.
+ */
+ public Date getLeftTime() {
+ return leftTime;
+ }
+
+ /**
+ * Returns a list of AgentDetails. For each Agent that was involved in the conversation
+ * the list will include an AgentDetail. An AgentDetail contains the JID of the agent
+ * as well as the time when the Agent joined and left the conversation.
+ *
+ * @return a list of AgentDetails.
+ */
+ public List<AgentDetail> getAgentDetails() {
+ return agentDetails;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<transcript sessionID=\"")
+ .append(sessionID)
+ .append("\">");
+
+ if (joinTime != null) {
+ buf.append("<joinTime>").append(UTC_FORMAT.format(joinTime)).append("</joinTime>");
+ }
+ if (leftTime != null) {
+ buf.append("<leftTime>").append(UTC_FORMAT.format(leftTime)).append("</leftTime>");
+ }
+ buf.append("<agents>");
+ for (AgentDetail agentDetail : agentDetails) {
+ buf.append(agentDetail.toXML());
+ }
+ buf.append("</agents></transcript>");
+
+ return buf.toString();
+ }
+ }
+
+ /**
+ * An AgentDetail contains information of an Agent that was involved in a conversation.
+ */
+ public static class AgentDetail {
+ private String agentJID;
+ private Date joinTime;
+ private Date leftTime;
+
+ public AgentDetail(String agentJID, Date joinTime, Date leftTime) {
+ this.agentJID = agentJID;
+ this.joinTime = joinTime;
+ this.leftTime = leftTime;
+ }
+
+ /**
+ * Returns the bare JID of the Agent that was involved in the conversation.
+ *
+ * @return the bared JID of the Agent that was involved in the conversation.
+ */
+ public String getAgentJID() {
+ return agentJID;
+ }
+
+ /**
+ * Returns the Date when the Agent joined the conversation.
+ *
+ * @return the Date when the Agent joined the conversation.
+ */
+ public Date getJoinTime() {
+ return joinTime;
+ }
+
+ /**
+ * Returns the Date when the Agent left the conversation.
+ *
+ * @return the Date when the Agent left the conversation.
+ */
+ public Date getLeftTime() {
+ return leftTime;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<agent>");
+
+ if (agentJID != null) {
+ buf.append("<agentJID>").append(agentJID).append("</agentJID>");
+ }
+ if (joinTime != null) {
+ buf.append("<joinTime>").append(UTC_FORMAT.format(joinTime)).append("</joinTime>");
+ }
+ if (leftTime != null) {
+ buf.append("<leftTime>").append(UTC_FORMAT.format(leftTime)).append("</leftTime>");
+ }
+ buf.append("</agent>");
+
+ return buf.toString();
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/TranscriptsProvider.java b/src/org/jivesoftware/smackx/workgroup/packet/TranscriptsProvider.java new file mode 100644 index 0000000..cb8f429 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/TranscriptsProvider.java @@ -0,0 +1,148 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+
+/**
+ * An IQProvider for transcripts summaries.
+ *
+ * @author Gaston Dombiak
+ */
+public class TranscriptsProvider implements IQProvider {
+
+ private static final SimpleDateFormat UTC_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
+ static {
+ UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+0"));
+ }
+
+ public TranscriptsProvider() {
+ super();
+ }
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ String userID = parser.getAttributeValue("", "userID");
+ List<Transcripts.TranscriptSummary> summaries = new ArrayList<Transcripts.TranscriptSummary>();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("transcript")) {
+ summaries.add(parseSummary(parser));
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("transcripts")) {
+ done = true;
+ }
+ }
+ }
+
+ return new Transcripts(userID, summaries);
+ }
+
+ private Transcripts.TranscriptSummary parseSummary(XmlPullParser parser) throws IOException,
+ XmlPullParserException {
+ String sessionID = parser.getAttributeValue("", "sessionID");
+ Date joinTime = null;
+ Date leftTime = null;
+ List<Transcripts.AgentDetail> agents = new ArrayList<Transcripts.AgentDetail>();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("joinTime")) {
+ try {
+ joinTime = UTC_FORMAT.parse(parser.nextText());
+ } catch (ParseException e) {}
+ }
+ else if (parser.getName().equals("leftTime")) {
+ try {
+ leftTime = UTC_FORMAT.parse(parser.nextText());
+ } catch (ParseException e) {}
+ }
+ else if (parser.getName().equals("agents")) {
+ agents = parseAgents(parser);
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("transcript")) {
+ done = true;
+ }
+ }
+ }
+
+ return new Transcripts.TranscriptSummary(sessionID, joinTime, leftTime, agents);
+ }
+
+ private List<Transcripts.AgentDetail> parseAgents(XmlPullParser parser) throws IOException, XmlPullParserException {
+ List<Transcripts.AgentDetail> agents = new ArrayList<Transcripts.AgentDetail>();
+ String agentJID = null;
+ Date joinTime = null;
+ Date leftTime = null;
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("agentJID")) {
+ agentJID = parser.nextText();
+ }
+ else if (parser.getName().equals("joinTime")) {
+ try {
+ joinTime = UTC_FORMAT.parse(parser.nextText());
+ } catch (ParseException e) {}
+ }
+ else if (parser.getName().equals("leftTime")) {
+ try {
+ leftTime = UTC_FORMAT.parse(parser.nextText());
+ } catch (ParseException e) {}
+ }
+ else if (parser.getName().equals("agent")) {
+ agentJID = null;
+ joinTime = null;
+ leftTime = null;
+ }
+ }
+ else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("agents")) {
+ done = true;
+ }
+ else if (parser.getName().equals("agent")) {
+ agents.add(new Transcripts.AgentDetail(agentJID, joinTime, leftTime));
+ }
+ }
+ }
+ return agents;
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/UserID.java b/src/org/jivesoftware/smackx/workgroup/packet/UserID.java new file mode 100644 index 0000000..8bf4589 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/UserID.java @@ -0,0 +1,77 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+public class UserID implements PacketExtension {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "user";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+ private String userID;
+
+ public UserID(String userID) {
+ this.userID = userID;
+ }
+
+ public String getUserID() {
+ return this.userID;
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\" ");
+ buf.append("id=\"").append(this.getUserID());
+ buf.append("\"/>");
+
+ return buf.toString();
+ }
+
+ public static class Provider implements PacketExtensionProvider {
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+ String userID = parser.getAttributeValue("", "id");
+
+ // Advance to end of extension.
+ parser.next();
+
+ return new UserID(userID);
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/WorkgroupInformation.java b/src/org/jivesoftware/smackx/workgroup/packet/WorkgroupInformation.java new file mode 100644 index 0000000..b0ea447 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/packet/WorkgroupInformation.java @@ -0,0 +1,86 @@ +/**
+ * $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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * A packet extension that contains information about the user and agent in a
+ * workgroup chat. The packet extension is attached to group chat invitations.
+ */
+public class WorkgroupInformation implements PacketExtension {
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "workgroup";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";
+
+ private String workgroupJID;
+
+ public WorkgroupInformation(String workgroupJID){
+ this.workgroupJID = workgroupJID;
+ }
+
+ public String getWorkgroupJID() {
+ return workgroupJID;
+ }
+
+ public String getElementName() {
+ return ELEMENT_NAME;
+ }
+
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ public String toXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append('<').append(ELEMENT_NAME);
+ buf.append(" jid=\"").append(getWorkgroupJID()).append("\"");
+ buf.append(" xmlns=\"").append(NAMESPACE).append("\" />");
+
+ return buf.toString();
+ }
+
+ public static class Provider implements PacketExtensionProvider {
+
+ /**
+ * PacketExtensionProvider implementation
+ */
+ public PacketExtension parseExtension (XmlPullParser parser)
+ throws Exception {
+ String workgroupJID = parser.getAttributeValue("", "jid");
+
+ // since this is a start and end tag, and we arrive on the start, this should guarantee
+ // we leave on the end
+ parser.next();
+
+ return new WorkgroupInformation(workgroupJID);
+ }
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/settings/ChatSetting.java b/src/org/jivesoftware/smackx/workgroup/settings/ChatSetting.java new file mode 100644 index 0000000..921134a --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/settings/ChatSetting.java @@ -0,0 +1,56 @@ +/**
+ * $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.smackx.workgroup.settings;
+
+public class ChatSetting {
+ private String key;
+ private String value;
+ private int type;
+
+ public ChatSetting(String key, String value, int type){
+ setKey(key);
+ setValue(value);
+ setType(type);
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public int getType() {
+ return type;
+ }
+
+ public void setType(int type) {
+ this.type = type;
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/settings/ChatSettings.java b/src/org/jivesoftware/smackx/workgroup/settings/ChatSettings.java new file mode 100644 index 0000000..ccc7a40 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/settings/ChatSettings.java @@ -0,0 +1,179 @@ +/**
+ * $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.smackx.workgroup.settings;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+public class ChatSettings extends IQ {
+
+ /**
+ * Defined as image type.
+ */
+ public static final int IMAGE_SETTINGS = 0;
+
+ /**
+ * Defined as Text settings type.
+ */
+ public static final int TEXT_SETTINGS = 1;
+
+ /**
+ * Defined as Bot settings type.
+ */
+ public static final int BOT_SETTINGS = 2;
+
+ private List<ChatSetting> settings;
+ private String key;
+ private int type = -1;
+
+ public ChatSettings() {
+ settings = new ArrayList<ChatSetting>();
+ }
+
+ public ChatSettings(String key) {
+ setKey(key);
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public void setType(int type) {
+ this.type = type;
+ }
+
+ public void addSetting(ChatSetting setting) {
+ settings.add(setting);
+ }
+
+ public Collection<ChatSetting> getSettings() {
+ return settings;
+ }
+
+ public ChatSetting getChatSetting(String key) {
+ Collection<ChatSetting> col = getSettings();
+ if (col != null) {
+ Iterator<ChatSetting> iter = col.iterator();
+ while (iter.hasNext()) {
+ ChatSetting chatSetting = iter.next();
+ if (chatSetting.getKey().equals(key)) {
+ return chatSetting;
+ }
+ }
+ }
+ return null;
+ }
+
+ public ChatSetting getFirstEntry() {
+ if (settings.size() > 0) {
+ return (ChatSetting)settings.get(0);
+ }
+ return null;
+ }
+
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "chat-settings";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=");
+ buf.append('"');
+ buf.append(NAMESPACE);
+ buf.append('"');
+ if (key != null) {
+ buf.append(" key=\"" + key + "\"");
+ }
+
+ if (type != -1) {
+ buf.append(" type=\"" + type + "\"");
+ }
+
+ buf.append("></").append(ELEMENT_NAME).append("> ");
+ return buf.toString();
+ }
+
+ /**
+ * Packet extension provider for AgentStatusRequest packets.
+ */
+ public static class InternalProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ throw new IllegalStateException("Parser not in proper position, or bad XML.");
+ }
+
+ ChatSettings chatSettings = new ChatSettings();
+
+ boolean done = false;
+ while (!done) {
+ int eventType = parser.next();
+ if ((eventType == XmlPullParser.START_TAG) && ("chat-setting".equals(parser.getName()))) {
+ chatSettings.addSetting(parseChatSetting(parser));
+
+ }
+ else if (eventType == XmlPullParser.END_TAG && ELEMENT_NAME.equals(parser.getName())) {
+ done = true;
+ }
+ }
+ return chatSettings;
+ }
+
+ private ChatSetting parseChatSetting(XmlPullParser parser) throws Exception {
+
+ boolean done = false;
+ String key = null;
+ String value = null;
+ int type = 0;
+
+ while (!done) {
+ int eventType = parser.next();
+ if ((eventType == XmlPullParser.START_TAG) && ("key".equals(parser.getName()))) {
+ key = parser.nextText();
+ }
+ else if ((eventType == XmlPullParser.START_TAG) && ("value".equals(parser.getName()))) {
+ value = parser.nextText();
+ }
+ else if ((eventType == XmlPullParser.START_TAG) && ("type".equals(parser.getName()))) {
+ type = Integer.parseInt(parser.nextText());
+ }
+ else if (eventType == XmlPullParser.END_TAG && "chat-setting".equals(parser.getName())) {
+ done = true;
+ }
+ }
+ return new ChatSetting(key, value, type);
+ }
+ }
+}
+
diff --git a/src/org/jivesoftware/smackx/workgroup/settings/GenericSettings.java b/src/org/jivesoftware/smackx/workgroup/settings/GenericSettings.java new file mode 100644 index 0000000..702eeb7 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/settings/GenericSettings.java @@ -0,0 +1,114 @@ +/** + * $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.smackx.workgroup.settings; + +import org.jivesoftware.smackx.workgroup.util.ModelUtil; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.xmlpull.v1.XmlPullParser; + +import java.util.HashMap; +import java.util.Map; + +public class GenericSettings extends IQ { + + private Map<String, String> map = new HashMap<String, String>(); + + private String query; + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public Map<String, String> getMap() { + return map; + } + + public void setMap(Map<String, String> map) { + this.map = map; + } + + + /** + * Element name of the packet extension. + */ + public static final String ELEMENT_NAME = "generic-metadata"; + + /** + * Namespace of the packet extension. + */ + public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup"; + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + + buf.append("<").append(ELEMENT_NAME).append(" xmlns="); + buf.append('"'); + buf.append(NAMESPACE); + buf.append('"'); + buf.append(">"); + if (ModelUtil.hasLength(getQuery())) { + buf.append("<query>" + getQuery() + "</query>"); + } + buf.append("</").append(ELEMENT_NAME).append("> "); + return buf.toString(); + } + + + /** + * Packet extension provider for SoundSetting Packets. + */ + public static class InternalProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + if (parser.getEventType() != XmlPullParser.START_TAG) { + throw new IllegalStateException("Parser not in proper position, or bad XML."); + } + + GenericSettings setting = new GenericSettings(); + + boolean done = false; + + + while (!done) { + int eventType = parser.next(); + if ((eventType == XmlPullParser.START_TAG) && ("entry".equals(parser.getName()))) { + eventType = parser.next(); + String name = parser.nextText(); + eventType = parser.next(); + String value = parser.nextText(); + setting.getMap().put(name, value); + } + else if (eventType == XmlPullParser.END_TAG && ELEMENT_NAME.equals(parser.getName())) { + done = true; + } + } + + return setting; + } + } + + +} + diff --git a/src/org/jivesoftware/smackx/workgroup/settings/OfflineSettings.java b/src/org/jivesoftware/smackx/workgroup/settings/OfflineSettings.java new file mode 100644 index 0000000..15136fd --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/settings/OfflineSettings.java @@ -0,0 +1,155 @@ +/**
+ * $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.smackx.workgroup.settings;
+
+import org.jivesoftware.smackx.workgroup.util.ModelUtil;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+public class OfflineSettings extends IQ {
+ private String redirectURL;
+
+ private String offlineText;
+ private String emailAddress;
+ private String subject;
+
+ public String getRedirectURL() {
+ if (!ModelUtil.hasLength(redirectURL)) {
+ return "";
+ }
+ return redirectURL;
+ }
+
+ public void setRedirectURL(String redirectURL) {
+ this.redirectURL = redirectURL;
+ }
+
+ public String getOfflineText() {
+ if (!ModelUtil.hasLength(offlineText)) {
+ return "";
+ }
+ return offlineText;
+ }
+
+ public void setOfflineText(String offlineText) {
+ this.offlineText = offlineText;
+ }
+
+ public String getEmailAddress() {
+ if (!ModelUtil.hasLength(emailAddress)) {
+ return "";
+ }
+ return emailAddress;
+ }
+
+ public void setEmailAddress(String emailAddress) {
+ this.emailAddress = emailAddress;
+ }
+
+ public String getSubject() {
+ if (!ModelUtil.hasLength(subject)) {
+ return "";
+ }
+ return subject;
+ }
+
+ public void setSubject(String subject) {
+ this.subject = subject;
+ }
+
+ public boolean redirects() {
+ return (ModelUtil.hasLength(getRedirectURL()));
+ }
+
+ public boolean isConfigured(){
+ return ModelUtil.hasLength(getEmailAddress()) &&
+ ModelUtil.hasLength(getSubject()) &&
+ ModelUtil.hasLength(getOfflineText());
+ }
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "offline-settings";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=");
+ buf.append('"');
+ buf.append(NAMESPACE);
+ buf.append('"');
+ buf.append("></").append(ELEMENT_NAME).append("> ");
+ return buf.toString();
+ }
+
+
+ /**
+ * Packet extension provider for AgentStatusRequest packets.
+ */
+ public static class InternalProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ throw new IllegalStateException("Parser not in proper position, or bad XML.");
+ }
+
+ OfflineSettings offlineSettings = new OfflineSettings();
+
+ boolean done = false;
+ String redirectPage = null;
+ String subject = null;
+ String offlineText = null;
+ String emailAddress = null;
+
+ while (!done) {
+ int eventType = parser.next();
+ if ((eventType == XmlPullParser.START_TAG) && ("redirectPage".equals(parser.getName()))) {
+ redirectPage = parser.nextText();
+ }
+ else if ((eventType == XmlPullParser.START_TAG) && ("subject".equals(parser.getName()))) {
+ subject = parser.nextText();
+ }
+ else if ((eventType == XmlPullParser.START_TAG) && ("offlineText".equals(parser.getName()))) {
+ offlineText = parser.nextText();
+ }
+ else if ((eventType == XmlPullParser.START_TAG) && ("emailAddress".equals(parser.getName()))) {
+ emailAddress = parser.nextText();
+ }
+ else if (eventType == XmlPullParser.END_TAG && "offline-settings".equals(parser.getName())) {
+ done = true;
+ }
+ }
+
+ offlineSettings.setEmailAddress(emailAddress);
+ offlineSettings.setRedirectURL(redirectPage);
+ offlineSettings.setSubject(subject);
+ offlineSettings.setOfflineText(offlineText);
+ return offlineSettings;
+ }
+ }
+}
+
diff --git a/src/org/jivesoftware/smackx/workgroup/settings/SearchSettings.java b/src/org/jivesoftware/smackx/workgroup/settings/SearchSettings.java new file mode 100644 index 0000000..98d59fc --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/settings/SearchSettings.java @@ -0,0 +1,112 @@ +/**
+ * 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.smackx.workgroup.settings;
+
+import org.jivesoftware.smackx.workgroup.util.ModelUtil;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+public class SearchSettings extends IQ {
+ private String forumsLocation;
+ private String kbLocation;
+
+ public boolean isSearchEnabled() {
+ return ModelUtil.hasLength(getForumsLocation()) && ModelUtil.hasLength(getKbLocation());
+ }
+
+ public String getForumsLocation() {
+ return forumsLocation;
+ }
+
+ public void setForumsLocation(String forumsLocation) {
+ this.forumsLocation = forumsLocation;
+ }
+
+ public String getKbLocation() {
+ return kbLocation;
+ }
+
+ public void setKbLocation(String kbLocation) {
+ this.kbLocation = kbLocation;
+ }
+
+ public boolean hasKB(){
+ return ModelUtil.hasLength(getKbLocation());
+ }
+
+ public boolean hasForums(){
+ return ModelUtil.hasLength(getForumsLocation());
+ }
+
+
+ /**
+ * Element name of the packet extension.
+ */
+ public static final String ELEMENT_NAME = "search-settings";
+
+ /**
+ * Namespace of the packet extension.
+ */
+ public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<").append(ELEMENT_NAME).append(" xmlns=");
+ buf.append('"');
+ buf.append(NAMESPACE);
+ buf.append('"');
+ buf.append("></").append(ELEMENT_NAME).append("> ");
+ return buf.toString();
+ }
+
+
+ /**
+ * Packet extension provider for AgentStatusRequest packets.
+ */
+ public static class InternalProvider implements IQProvider {
+
+ public IQ parseIQ(XmlPullParser parser) throws Exception {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ throw new IllegalStateException("Parser not in proper position, or bad XML.");
+ }
+
+ SearchSettings settings = new SearchSettings();
+
+ boolean done = false;
+ String kb = null;
+ String forums = null;
+
+ while (!done) {
+ int eventType = parser.next();
+ if ((eventType == XmlPullParser.START_TAG) && ("forums".equals(parser.getName()))) {
+ forums = parser.nextText();
+ }
+ else if ((eventType == XmlPullParser.START_TAG) && ("kb".equals(parser.getName()))) {
+ kb = parser.nextText();
+ }
+ else if (eventType == XmlPullParser.END_TAG && "search-settings".equals(parser.getName())) {
+ done = true;
+ }
+ }
+
+ settings.setForumsLocation(forums);
+ settings.setKbLocation(kb);
+ return settings;
+ }
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/settings/SoundSettings.java b/src/org/jivesoftware/smackx/workgroup/settings/SoundSettings.java new file mode 100644 index 0000000..66bec35 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/settings/SoundSettings.java @@ -0,0 +1,103 @@ +/** + * $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.smackx.workgroup.settings; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.util.StringUtils; +import org.xmlpull.v1.XmlPullParser; + +public class SoundSettings extends IQ { + private String outgoingSound; + private String incomingSound; + + + public void setOutgoingSound(String outgoingSound) { + this.outgoingSound = outgoingSound; + } + + public void setIncomingSound(String incomingSound) { + this.incomingSound = incomingSound; + } + + public byte[] getIncomingSoundBytes() { + return StringUtils.decodeBase64(incomingSound); + } + + public byte[] getOutgoingSoundBytes() { + return StringUtils.decodeBase64(outgoingSound); + } + + + /** + * Element name of the packet extension. + */ + public static final String ELEMENT_NAME = "sound-settings"; + + /** + * Namespace of the packet extension. + */ + public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup"; + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + + buf.append("<").append(ELEMENT_NAME).append(" xmlns="); + buf.append('"'); + buf.append(NAMESPACE); + buf.append('"'); + buf.append("></").append(ELEMENT_NAME).append("> "); + return buf.toString(); + } + + + /** + * Packet extension provider for SoundSetting Packets. + */ + public static class InternalProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + if (parser.getEventType() != XmlPullParser.START_TAG) { + throw new IllegalStateException("Parser not in proper position, or bad XML."); + } + + SoundSettings soundSettings = new SoundSettings(); + + boolean done = false; + + + while (!done) { + int eventType = parser.next(); + if ((eventType == XmlPullParser.START_TAG) && ("outgoingSound".equals(parser.getName()))) { + soundSettings.setOutgoingSound(parser.nextText()); + } + else if ((eventType == XmlPullParser.START_TAG) && ("incomingSound".equals(parser.getName()))) { + soundSettings.setIncomingSound(parser.nextText()); + } + else if (eventType == XmlPullParser.END_TAG && "sound-settings".equals(parser.getName())) { + done = true; + } + } + + return soundSettings; + } + } +} + diff --git a/src/org/jivesoftware/smackx/workgroup/settings/WorkgroupProperties.java b/src/org/jivesoftware/smackx/workgroup/settings/WorkgroupProperties.java new file mode 100644 index 0000000..8e405bb --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/settings/WorkgroupProperties.java @@ -0,0 +1,125 @@ +/** + * $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.smackx.workgroup.settings; + +import org.jivesoftware.smackx.workgroup.util.ModelUtil; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.provider.IQProvider; +import org.xmlpull.v1.XmlPullParser; + +public class WorkgroupProperties extends IQ { + + private boolean authRequired; + private String email; + private String fullName; + private String jid; + + public boolean isAuthRequired() { + return authRequired; + } + + public void setAuthRequired(boolean authRequired) { + this.authRequired = authRequired; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getJid() { + return jid; + } + + public void setJid(String jid) { + this.jid = jid; + } + + + /** + * Element name of the packet extension. + */ + public static final String ELEMENT_NAME = "workgroup-properties"; + + /** + * Namespace of the packet extension. + */ + public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup"; + + public String getChildElementXML() { + StringBuilder buf = new StringBuilder(); + + buf.append("<").append(ELEMENT_NAME).append(" xmlns="); + buf.append('"'); + buf.append(NAMESPACE); + buf.append('"'); + if (ModelUtil.hasLength(getJid())) { + buf.append("jid=\"" + getJid() + "\" "); + } + buf.append("></").append(ELEMENT_NAME).append("> "); + return buf.toString(); + } + + /** + * Packet extension provider for SoundSetting Packets. + */ + public static class InternalProvider implements IQProvider { + + public IQ parseIQ(XmlPullParser parser) throws Exception { + if (parser.getEventType() != XmlPullParser.START_TAG) { + throw new IllegalStateException("Parser not in proper position, or bad XML."); + } + + WorkgroupProperties props = new WorkgroupProperties(); + + boolean done = false; + + + while (!done) { + int eventType = parser.next(); + if ((eventType == XmlPullParser.START_TAG) && ("authRequired".equals(parser.getName()))) { + props.setAuthRequired(new Boolean(parser.nextText()).booleanValue()); + } + else if ((eventType == XmlPullParser.START_TAG) && ("email".equals(parser.getName()))) { + props.setEmail(parser.nextText()); + } + else if ((eventType == XmlPullParser.START_TAG) && ("name".equals(parser.getName()))) { + props.setFullName(parser.nextText()); + } + else if (eventType == XmlPullParser.END_TAG && "workgroup-properties".equals(parser.getName())) { + done = true; + } + } + + return props; + } + } +} diff --git a/src/org/jivesoftware/smackx/workgroup/user/QueueListener.java b/src/org/jivesoftware/smackx/workgroup/user/QueueListener.java new file mode 100644 index 0000000..fa3e6a6 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/user/QueueListener.java @@ -0,0 +1,55 @@ +/**
+ * $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.smackx.workgroup.user;
+
+/**
+ * Listener interface for those that wish to be notified of workgroup queue events.
+ *
+ * @see Workgroup#addQueueListener(QueueListener)
+ * @author loki der quaeler
+ */
+public interface QueueListener {
+
+ /**
+ * The user joined the workgroup queue.
+ */
+ public void joinedQueue();
+
+ /**
+ * The user departed the workgroup queue.
+ */
+ public void departedQueue();
+
+ /**
+ * The user's queue position has been updated to a new value.
+ *
+ * @param currentPosition the user's current position in the queue.
+ */
+ public void queuePositionUpdated(int currentPosition);
+
+ /**
+ * The user's estimated remaining wait time in the queue has been updated.
+ *
+ * @param secondsRemaining the estimated number of seconds remaining until the
+ * the user is routed to the agent.
+ */
+ public void queueWaitTimeUpdated(int secondsRemaining);
+
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/user/Workgroup.java b/src/org/jivesoftware/smackx/workgroup/user/Workgroup.java new file mode 100644 index 0000000..237337f --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/user/Workgroup.java @@ -0,0 +1,868 @@ +/**
+ * 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.smackx.workgroup.user;
+
+import org.jivesoftware.smackx.workgroup.MetaData;
+import org.jivesoftware.smackx.workgroup.WorkgroupInvitation;
+import org.jivesoftware.smackx.workgroup.WorkgroupInvitationListener;
+import org.jivesoftware.smackx.workgroup.ext.forms.WorkgroupForm;
+import org.jivesoftware.smackx.workgroup.packet.DepartQueuePacket;
+import org.jivesoftware.smackx.workgroup.packet.QueueUpdate;
+import org.jivesoftware.smackx.workgroup.packet.SessionID;
+import org.jivesoftware.smackx.workgroup.packet.UserID;
+import org.jivesoftware.smackx.workgroup.settings.*;
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.muc.MultiUserChat;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.MUCUser;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Provides workgroup services for users. Users can join the workgroup queue, depart the
+ * queue, find status information about their placement in the queue, and register to
+ * be notified when they are routed to an agent.<p>
+ * <p/>
+ * This class only provides a users perspective into a workgroup and is not intended
+ * for use by agents.
+ *
+ * @author Matt Tucker
+ * @author Derek DeMoro
+ */
+public class Workgroup {
+
+ private String workgroupJID;
+ private Connection connection;
+ private boolean inQueue;
+ private List<WorkgroupInvitationListener> invitationListeners;
+ private List<QueueListener> queueListeners;
+
+ private int queuePosition = -1;
+ private int queueRemainingTime = -1;
+
+ /**
+ * Creates a new workgroup instance using the specified workgroup JID
+ * (eg support@workgroup.example.com) and XMPP connection. The connection must have
+ * undergone a successful login before being used to construct an instance of
+ * this class.
+ *
+ * @param workgroupJID the JID of the workgroup.
+ * @param connection an XMPP connection which must have already undergone a
+ * successful login.
+ */
+ public Workgroup(String workgroupJID, Connection connection) {
+ // Login must have been done before passing in connection.
+ if (!connection.isAuthenticated()) {
+ throw new IllegalStateException("Must login to server before creating workgroup.");
+ }
+
+ this.workgroupJID = workgroupJID;
+ this.connection = connection;
+ inQueue = false;
+ invitationListeners = new ArrayList<WorkgroupInvitationListener>();
+ queueListeners = new ArrayList<QueueListener>();
+
+ // Register as a queue listener for internal usage by this instance.
+ addQueueListener(new QueueListener() {
+ public void joinedQueue() {
+ inQueue = true;
+ }
+
+ public void departedQueue() {
+ inQueue = false;
+ queuePosition = -1;
+ queueRemainingTime = -1;
+ }
+
+ public void queuePositionUpdated(int currentPosition) {
+ queuePosition = currentPosition;
+ }
+
+ public void queueWaitTimeUpdated(int secondsRemaining) {
+ queueRemainingTime = secondsRemaining;
+ }
+ });
+
+ /**
+ * Internal handling of an invitation.Recieving an invitation removes the user from the queue.
+ */
+ MultiUserChat.addInvitationListener(connection,
+ new org.jivesoftware.smackx.muc.InvitationListener() {
+ public void invitationReceived(Connection conn, String room, String inviter,
+ String reason, String password, Message message) {
+ inQueue = false;
+ queuePosition = -1;
+ queueRemainingTime = -1;
+ }
+ });
+
+ // Register a packet listener for all the messages sent to this client.
+ PacketFilter typeFilter = new PacketTypeFilter(Message.class);
+
+ connection.addPacketListener(new PacketListener() {
+ public void processPacket(Packet packet) {
+ handlePacket(packet);
+ }
+ }, typeFilter);
+ }
+
+ /**
+ * Returns the name of this workgroup (eg support@example.com).
+ *
+ * @return the name of the workgroup.
+ */
+ public String getWorkgroupJID() {
+ return workgroupJID;
+ }
+
+ /**
+ * Returns true if the user is currently waiting in the workgroup queue.
+ *
+ * @return true if currently waiting in the queue.
+ */
+ public boolean isInQueue() {
+ return inQueue;
+ }
+
+ /**
+ * Returns true if the workgroup is available for receiving new requests. The workgroup will be
+ * available only when agents are available for this workgroup.
+ *
+ * @return true if the workgroup is available for receiving new requests.
+ */
+ public boolean isAvailable() {
+ Presence directedPresence = new Presence(Presence.Type.available);
+ directedPresence.setTo(workgroupJID);
+ PacketFilter typeFilter = new PacketTypeFilter(Presence.class);
+ PacketFilter fromFilter = new FromContainsFilter(workgroupJID);
+ PacketCollector collector = connection.createPacketCollector(new AndFilter(fromFilter,
+ typeFilter));
+
+ connection.sendPacket(directedPresence);
+
+ Presence response = (Presence)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ return false;
+ }
+ else if (response.getError() != null) {
+ return false;
+ }
+ else {
+ return Presence.Type.available == response.getType();
+ }
+ }
+
+ /**
+ * Returns the users current position in the workgroup queue. A value of 0 means
+ * the user is next in line to be routed; therefore, if the queue position
+ * is being displayed to the end user it is usually a good idea to add 1 to
+ * the value this method returns before display. If the user is not currently
+ * waiting in the workgroup, or no queue position information is available, -1
+ * will be returned.
+ *
+ * @return the user's current position in the workgroup queue, or -1 if the
+ * position isn't available or if the user isn't in the queue.
+ */
+ public int getQueuePosition() {
+ return queuePosition;
+ }
+
+ /**
+ * Returns the estimated time (in seconds) that the user has to left wait in
+ * the workgroup queue before being routed. If the user is not currently waiting
+ * int he workgroup, or no queue time information is available, -1 will be
+ * returned.
+ *
+ * @return the estimated time remaining (in seconds) that the user has to
+ * wait inthe workgroupu queue, or -1 if time information isn't available
+ * or if the user isn't int the queue.
+ */
+ public int getQueueRemainingTime() {
+ return queueRemainingTime;
+ }
+
+ /**
+ * Joins the workgroup queue to wait to be routed to an agent. After joining
+ * the queue, queue status events will be sent to indicate the user's position and
+ * estimated time left in the queue. Once joining the queue, there are three ways
+ * the user can leave the queue: <ul>
+ * <p/>
+ * <li>The user is routed to an agent, which triggers a GroupChat invitation.
+ * <li>The user asks to leave the queue by calling the {@link #departQueue} method.
+ * <li>A server error occurs, or an administrator explicitly removes the user
+ * from the queue.
+ * </ul>
+ * <p/>
+ * A user cannot request to join the queue again if already in the queue. Therefore,
+ * this method will throw an IllegalStateException if the user is already in the queue.<p>
+ * <p/>
+ * Some servers may be configured to require certain meta-data in order to
+ * join the queue. In that case, the {@link #joinQueue(Form)} method should be
+ * used instead of this method so that meta-data may be passed in.<p>
+ * <p/>
+ * The server tracks the conversations that a user has with agents over time. By
+ * default, that tracking is done using the user's JID. However, this is not always
+ * possible. For example, when the user is logged in anonymously using a web client.
+ * In that case the user ID might be a randomly generated value put into a persistent
+ * cookie or a username obtained via the session. A userID can be explicitly
+ * passed in by using the {@link #joinQueue(Form, String)} method. When specified,
+ * that userID will be used instead of the user's JID to track conversations. The
+ * server will ignore a manually specified userID if the user's connection to the server
+ * is not anonymous.
+ *
+ * @throws XMPPException if an error occured joining the queue. An error may indicate
+ * that a connection failure occured or that the server explicitly rejected the
+ * request to join the queue.
+ */
+ public void joinQueue() throws XMPPException {
+ joinQueue(null);
+ }
+
+ /**
+ * Joins the workgroup queue to wait to be routed to an agent. After joining
+ * the queue, queue status events will be sent to indicate the user's position and
+ * estimated time left in the queue. Once joining the queue, there are three ways
+ * the user can leave the queue: <ul>
+ * <p/>
+ * <li>The user is routed to an agent, which triggers a GroupChat invitation.
+ * <li>The user asks to leave the queue by calling the {@link #departQueue} method.
+ * <li>A server error occurs, or an administrator explicitly removes the user
+ * from the queue.
+ * </ul>
+ * <p/>
+ * A user cannot request to join the queue again if already in the queue. Therefore,
+ * this method will throw an IllegalStateException if the user is already in the queue.<p>
+ * <p/>
+ * Some servers may be configured to require certain meta-data in order to
+ * join the queue.<p>
+ * <p/>
+ * The server tracks the conversations that a user has with agents over time. By
+ * default, that tracking is done using the user's JID. However, this is not always
+ * possible. For example, when the user is logged in anonymously using a web client.
+ * In that case the user ID might be a randomly generated value put into a persistent
+ * cookie or a username obtained via the session. A userID can be explicitly
+ * passed in by using the {@link #joinQueue(Form, String)} method. When specified,
+ * that userID will be used instead of the user's JID to track conversations. The
+ * server will ignore a manually specified userID if the user's connection to the server
+ * is not anonymous.
+ *
+ * @param answerForm the completed form the send for the join request.
+ * @throws XMPPException if an error occured joining the queue. An error may indicate
+ * that a connection failure occured or that the server explicitly rejected the
+ * request to join the queue.
+ */
+ public void joinQueue(Form answerForm) throws XMPPException {
+ joinQueue(answerForm, null);
+ }
+
+ /**
+ * <p>Joins the workgroup queue to wait to be routed to an agent. After joining
+ * the queue, queue status events will be sent to indicate the user's position and
+ * estimated time left in the queue. Once joining the queue, there are three ways
+ * the user can leave the queue: <ul>
+ * <p/>
+ * <li>The user is routed to an agent, which triggers a GroupChat invitation.
+ * <li>The user asks to leave the queue by calling the {@link #departQueue} method.
+ * <li>A server error occurs, or an administrator explicitly removes the user
+ * from the queue.
+ * </ul>
+ * <p/>
+ * A user cannot request to join the queue again if already in the queue. Therefore,
+ * this method will throw an IllegalStateException if the user is already in the queue.<p>
+ * <p/>
+ * Some servers may be configured to require certain meta-data in order to
+ * join the queue.<p>
+ * <p/>
+ * The server tracks the conversations that a user has with agents over time. By
+ * default, that tracking is done using the user's JID. However, this is not always
+ * possible. For example, when the user is logged in anonymously using a web client.
+ * In that case the user ID might be a randomly generated value put into a persistent
+ * cookie or a username obtained via the session. When specified, that userID will
+ * be used instead of the user's JID to track conversations. The server will ignore a
+ * manually specified userID if the user's connection to the server is not anonymous.
+ *
+ * @param answerForm the completed form associated with the join reqest.
+ * @param userID String that represents the ID of the user when using anonymous sessions
+ * or <tt>null</tt> if a userID should not be used.
+ * @throws XMPPException if an error occured joining the queue. An error may indicate
+ * that a connection failure occured or that the server explicitly rejected the
+ * request to join the queue.
+ */
+ public void joinQueue(Form answerForm, String userID) throws XMPPException {
+ // If already in the queue ignore the join request.
+ if (inQueue) {
+ throw new IllegalStateException("Already in queue " + workgroupJID);
+ }
+
+ JoinQueuePacket joinPacket = new JoinQueuePacket(workgroupJID, answerForm, userID);
+
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(joinPacket.getPacketID()));
+
+ this.connection.sendPacket(joinPacket);
+
+ IQ response = (IQ)collector.nextResult(10000);
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+
+ // Notify listeners that we've joined the queue.
+ fireQueueJoinedEvent();
+ }
+
+ /**
+ * <p>Joins the workgroup queue to wait to be routed to an agent. After joining
+ * the queue, queue status events will be sent to indicate the user's position and
+ * estimated time left in the queue. Once joining the queue, there are three ways
+ * the user can leave the queue: <ul>
+ * <p/>
+ * <li>The user is routed to an agent, which triggers a GroupChat invitation.
+ * <li>The user asks to leave the queue by calling the {@link #departQueue} method.
+ * <li>A server error occurs, or an administrator explicitly removes the user
+ * from the queue.
+ * </ul>
+ * <p/>
+ * A user cannot request to join the queue again if already in the queue. Therefore,
+ * this method will throw an IllegalStateException if the user is already in the queue.<p>
+ * <p/>
+ * Some servers may be configured to require certain meta-data in order to
+ * join the queue.<p>
+ * <p/>
+ * The server tracks the conversations that a user has with agents over time. By
+ * default, that tracking is done using the user's JID. However, this is not always
+ * possible. For example, when the user is logged in anonymously using a web client.
+ * In that case the user ID might be a randomly generated value put into a persistent
+ * cookie or a username obtained via the session. When specified, that userID will
+ * be used instead of the user's JID to track conversations. The server will ignore a
+ * manually specified userID if the user's connection to the server is not anonymous.
+ *
+ * @param metadata metadata to create a dataform from.
+ * @param userID String that represents the ID of the user when using anonymous sessions
+ * or <tt>null</tt> if a userID should not be used.
+ * @throws XMPPException if an error occured joining the queue. An error may indicate
+ * that a connection failure occured or that the server explicitly rejected the
+ * request to join the queue.
+ */
+ public void joinQueue(Map<String,Object> metadata, String userID) throws XMPPException {
+ // If already in the queue ignore the join request.
+ if (inQueue) {
+ throw new IllegalStateException("Already in queue " + workgroupJID);
+ }
+
+ // Build dataform from metadata
+ Form form = new Form(Form.TYPE_SUBMIT);
+ Iterator<String> iter = metadata.keySet().iterator();
+ while (iter.hasNext()) {
+ String name = iter.next();
+ String value = metadata.get(name).toString();
+
+ String escapedName = StringUtils.escapeForXML(name);
+ String escapedValue = StringUtils.escapeForXML(value);
+
+ FormField field = new FormField(escapedName);
+ field.setType(FormField.TYPE_TEXT_SINGLE);
+ form.addField(field);
+ form.setAnswer(escapedName, escapedValue);
+ }
+ joinQueue(form, userID);
+ }
+
+ /**
+ * Departs the workgroup queue. If the user is not currently in the queue, this
+ * method will do nothing.<p>
+ * <p/>
+ * Normally, the user would not manually leave the queue. However, they may wish to
+ * under certain circumstances -- for example, if they no longer wish to be routed
+ * to an agent because they've been waiting too long.
+ *
+ * @throws XMPPException if an error occured trying to send the depart queue
+ * request to the server.
+ */
+ public void departQueue() throws XMPPException {
+ // If not in the queue ignore the depart request.
+ if (!inQueue) {
+ return;
+ }
+
+ DepartQueuePacket departPacket = new DepartQueuePacket(this.workgroupJID);
+ PacketCollector collector = this.connection.createPacketCollector(new PacketIDFilter(departPacket.getPacketID()));
+
+ connection.sendPacket(departPacket);
+
+ IQ response = (IQ)collector.nextResult(5000);
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from the server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+
+ // Notify listeners that we're no longer in the queue.
+ fireQueueDepartedEvent();
+ }
+
+ /**
+ * Adds a queue listener that will be notified of queue events for the user
+ * that created this Workgroup instance.
+ *
+ * @param queueListener the queue listener.
+ */
+ public void addQueueListener(QueueListener queueListener) {
+ synchronized (queueListeners) {
+ if (!queueListeners.contains(queueListener)) {
+ queueListeners.add(queueListener);
+ }
+ }
+ }
+
+ /**
+ * Removes a queue listener.
+ *
+ * @param queueListener the queue listener.
+ */
+ public void removeQueueListener(QueueListener queueListener) {
+ synchronized (queueListeners) {
+ queueListeners.remove(queueListener);
+ }
+ }
+
+ /**
+ * Adds an invitation listener that will be notified of groupchat invitations
+ * from the workgroup for the the user that created this Workgroup instance.
+ *
+ * @param invitationListener the invitation listener.
+ */
+ public void addInvitationListener(WorkgroupInvitationListener invitationListener) {
+ synchronized (invitationListeners) {
+ if (!invitationListeners.contains(invitationListener)) {
+ invitationListeners.add(invitationListener);
+ }
+ }
+ }
+
+ /**
+ * Removes an invitation listener.
+ *
+ * @param invitationListener the invitation listener.
+ */
+ public void removeQueueListener(WorkgroupInvitationListener invitationListener) {
+ synchronized (invitationListeners) {
+ invitationListeners.remove(invitationListener);
+ }
+ }
+
+ private void fireInvitationEvent(WorkgroupInvitation invitation) {
+ synchronized (invitationListeners) {
+ for (Iterator<WorkgroupInvitationListener> i = invitationListeners.iterator(); i.hasNext();) {
+ WorkgroupInvitationListener listener = i.next();
+ listener.invitationReceived(invitation);
+ }
+ }
+ }
+
+ private void fireQueueJoinedEvent() {
+ synchronized (queueListeners) {
+ for (Iterator<QueueListener> i = queueListeners.iterator(); i.hasNext();) {
+ QueueListener listener = i.next();
+ listener.joinedQueue();
+ }
+ }
+ }
+
+ private void fireQueueDepartedEvent() {
+ synchronized (queueListeners) {
+ for (Iterator<QueueListener> i = queueListeners.iterator(); i.hasNext();) {
+ QueueListener listener = i.next();
+ listener.departedQueue();
+ }
+ }
+ }
+
+ private void fireQueuePositionEvent(int currentPosition) {
+ synchronized (queueListeners) {
+ for (Iterator<QueueListener> i = queueListeners.iterator(); i.hasNext();) {
+ QueueListener listener = i.next();
+ listener.queuePositionUpdated(currentPosition);
+ }
+ }
+ }
+
+ private void fireQueueTimeEvent(int secondsRemaining) {
+ synchronized (queueListeners) {
+ for (Iterator<QueueListener> i = queueListeners.iterator(); i.hasNext();) {
+ QueueListener listener = i.next();
+ listener.queueWaitTimeUpdated(secondsRemaining);
+ }
+ }
+ }
+
+ // PacketListener Implementation.
+
+ private void handlePacket(Packet packet) {
+ if (packet instanceof Message) {
+ Message msg = (Message)packet;
+ // Check to see if the user left the queue.
+ PacketExtension pe = msg.getExtension("depart-queue", "http://jabber.org/protocol/workgroup");
+ PacketExtension queueStatus = msg.getExtension("queue-status", "http://jabber.org/protocol/workgroup");
+
+ if (pe != null) {
+ fireQueueDepartedEvent();
+ }
+ else if (queueStatus != null) {
+ QueueUpdate queueUpdate = (QueueUpdate)queueStatus;
+ if (queueUpdate.getPosition() != -1) {
+ fireQueuePositionEvent(queueUpdate.getPosition());
+ }
+ if (queueUpdate.getRemaingTime() != -1) {
+ fireQueueTimeEvent(queueUpdate.getRemaingTime());
+ }
+ }
+
+ else {
+ // Check if a room invitation was sent and if the sender is the workgroup
+ MUCUser mucUser = (MUCUser)msg.getExtension("x", "http://jabber.org/protocol/muc#user");
+ MUCUser.Invite invite = mucUser != null ? mucUser.getInvite() : null;
+ if (invite != null && workgroupJID.equals(invite.getFrom())) {
+ String sessionID = null;
+ Map<String, List<String>> metaData = null;
+
+ pe = msg.getExtension(SessionID.ELEMENT_NAME,
+ SessionID.NAMESPACE);
+ if (pe != null) {
+ sessionID = ((SessionID)pe).getSessionID();
+ }
+
+ pe = msg.getExtension(MetaData.ELEMENT_NAME,
+ MetaData.NAMESPACE);
+ if (pe != null) {
+ metaData = ((MetaData)pe).getMetaData();
+ }
+
+ WorkgroupInvitation inv = new WorkgroupInvitation(connection.getUser(), msg.getFrom(),
+ workgroupJID, sessionID, msg.getBody(),
+ msg.getFrom(), metaData);
+
+ fireInvitationEvent(inv);
+ }
+ }
+ }
+ }
+
+ /**
+ * IQ packet to request joining the workgroup queue.
+ */
+ private class JoinQueuePacket extends IQ {
+
+ private String userID = null;
+ private DataForm form;
+
+ public JoinQueuePacket(String workgroup, Form answerForm, String userID) {
+ this.userID = userID;
+
+ setTo(workgroup);
+ setType(IQ.Type.SET);
+
+ form = answerForm.getDataFormToSend();
+ addExtension(form);
+ }
+
+ public String getChildElementXML() {
+ StringBuilder buf = new StringBuilder();
+
+ buf.append("<join-queue xmlns=\"http://jabber.org/protocol/workgroup\">");
+ buf.append("<queue-notifications/>");
+ // Add the user unique identification if the session is anonymous
+ if (connection.isAnonymous()) {
+ buf.append(new UserID(userID).toXML());
+ }
+
+ // Append data form text
+ buf.append(form.toXML());
+
+ buf.append("</join-queue>");
+
+ return buf.toString();
+ }
+ }
+
+ /**
+ * Returns a single chat setting based on it's identified key.
+ *
+ * @param key the key to find.
+ * @return the ChatSetting if found, otherwise false.
+ * @throws XMPPException if an error occurs while getting information from the server.
+ */
+ public ChatSetting getChatSetting(String key) throws XMPPException {
+ ChatSettings chatSettings = getChatSettings(key, -1);
+ return chatSettings.getFirstEntry();
+ }
+
+ /**
+ * Returns ChatSettings based on type.
+ *
+ * @param type the type of ChatSettings to return.
+ * @return the ChatSettings of given type, otherwise null.
+ * @throws XMPPException if an error occurs while getting information from the server.
+ */
+ public ChatSettings getChatSettings(int type) throws XMPPException {
+ return getChatSettings(null, type);
+ }
+
+ /**
+ * Returns all ChatSettings.
+ *
+ * @return all ChatSettings of a given workgroup.
+ * @throws XMPPException if an error occurs while getting information from the server.
+ */
+ public ChatSettings getChatSettings() throws XMPPException {
+ return getChatSettings(null, -1);
+ }
+
+
+ /**
+ * Asks the workgroup for it's Chat Settings.
+ *
+ * @return key specify a key to retrieve only that settings. Otherwise for all settings, key should be null.
+ * @throws XMPPException if an error occurs while getting information from the server.
+ */
+ private ChatSettings getChatSettings(String key, int type) throws XMPPException {
+ ChatSettings request = new ChatSettings();
+ if (key != null) {
+ request.setKey(key);
+ }
+ if (type != -1) {
+ request.setType(type);
+ }
+ request.setType(IQ.Type.GET);
+ request.setTo(workgroupJID);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+
+ ChatSettings response = (ChatSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+ }
+
+ /**
+ * The workgroup service may be configured to send email. This queries the Workgroup Service
+ * to see if the email service has been configured and is available.
+ *
+ * @return true if the email service is available, otherwise return false.
+ */
+ public boolean isEmailAvailable() {
+ ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
+
+ try {
+ String workgroupService = StringUtils.parseServer(workgroupJID);
+ DiscoverInfo infoResult = discoManager.discoverInfo(workgroupService);
+ return infoResult.containsFeature("jive:email:provider");
+ }
+ catch (XMPPException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Asks the workgroup for it's Offline Settings.
+ *
+ * @return offlineSettings the offline settings for this workgroup.
+ * @throws XMPPException if an error occurs while getting information from the server.
+ */
+ public OfflineSettings getOfflineSettings() throws XMPPException {
+ OfflineSettings request = new OfflineSettings();
+ request.setType(IQ.Type.GET);
+ request.setTo(workgroupJID);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+
+ OfflineSettings response = (OfflineSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+ }
+
+ /**
+ * Asks the workgroup for it's Sound Settings.
+ *
+ * @return soundSettings the sound settings for the specified workgroup.
+ * @throws XMPPException if an error occurs while getting information from the server.
+ */
+ public SoundSettings getSoundSettings() throws XMPPException {
+ SoundSettings request = new SoundSettings();
+ request.setType(IQ.Type.GET);
+ request.setTo(workgroupJID);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+
+ SoundSettings response = (SoundSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+ }
+
+ /**
+ * Asks the workgroup for it's Properties
+ *
+ * @return the WorkgroupProperties for the specified workgroup.
+ * @throws XMPPException if an error occurs while getting information from the server.
+ */
+ public WorkgroupProperties getWorkgroupProperties() throws XMPPException {
+ WorkgroupProperties request = new WorkgroupProperties();
+ request.setType(IQ.Type.GET);
+ request.setTo(workgroupJID);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+
+ WorkgroupProperties response = (WorkgroupProperties)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+ }
+
+ /**
+ * Asks the workgroup for it's Properties
+ *
+ * @param jid the jid of the user who's information you would like the workgroup to retreive.
+ * @return the WorkgroupProperties for the specified workgroup.
+ * @throws XMPPException if an error occurs while getting information from the server.
+ */
+ public WorkgroupProperties getWorkgroupProperties(String jid) throws XMPPException {
+ WorkgroupProperties request = new WorkgroupProperties();
+ request.setJid(jid);
+ request.setType(IQ.Type.GET);
+ request.setTo(workgroupJID);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
+ connection.sendPacket(request);
+
+
+ WorkgroupProperties response = (WorkgroupProperties)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return response;
+ }
+
+
+ /**
+ * Returns the Form to use for all clients of a workgroup. It is unlikely that the server
+ * will change the form (without a restart) so it is safe to keep the returned form
+ * for future submissions.
+ *
+ * @return the Form to use for searching transcripts.
+ * @throws XMPPException if an error occurs while sending the request to the server.
+ */
+ public Form getWorkgroupForm() throws XMPPException {
+ WorkgroupForm workgroupForm = new WorkgroupForm();
+ workgroupForm.setType(IQ.Type.GET);
+ workgroupForm.setTo(workgroupJID);
+
+ PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(workgroupForm.getPacketID()));
+ connection.sendPacket(workgroupForm);
+
+ WorkgroupForm response = (WorkgroupForm)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+ // Cancel the collector.
+ collector.cancel();
+ if (response == null) {
+ throw new XMPPException("No response from server on status set.");
+ }
+ if (response.getError() != null) {
+ throw new XMPPException(response.getError());
+ }
+ return Form.getFormFrom(response);
+ }
+
+ /*
+ public static void main(String args[]) throws Exception {
+ Connection con = new XMPPConnection("anteros");
+ con.connect();
+ con.loginAnonymously();
+
+ Workgroup workgroup = new Workgroup("demo@workgroup.anteros", con);
+ WorkgroupProperties props = workgroup.getWorkgroupProperties("derek@anteros.com");
+
+ System.out.print(props);
+ con.disconnect();
+ }
+ */
+
+
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/util/ListenerEventDispatcher.java b/src/org/jivesoftware/smackx/workgroup/util/ListenerEventDispatcher.java new file mode 100644 index 0000000..533b9a1 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/util/ListenerEventDispatcher.java @@ -0,0 +1,132 @@ +/**
+ * 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.smackx.workgroup.util;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.ListIterator;
+
+/**
+ * This class is a very flexible event dispatcher which implements Runnable so that it can
+ * dispatch easily from a newly created thread. The usage of this in code is more or less:
+ * create a new instance of this class, use addListenerTriplet to add as many listeners
+ * as desired to be messaged, create a new Thread using the instance of this class created
+ * as the argument to the constructor, start the new Thread instance.<p>
+ *
+ * Also, this is intended to be used to message methods that either return void, or have
+ * a return which the developer using this class is uninterested in receiving.
+ *
+ * @author loki der quaeler
+ */
+public class ListenerEventDispatcher
+ implements Runnable {
+
+ protected transient ArrayList<TripletContainer> triplets;
+
+ protected transient boolean hasFinishedDispatching;
+ protected transient boolean isRunning;
+
+ public ListenerEventDispatcher () {
+ super();
+
+ this.triplets = new ArrayList<TripletContainer>();
+
+ this.hasFinishedDispatching = false;
+ this.isRunning = false;
+ }
+
+ /**
+ * Add a listener triplet - the instance of the listener to be messaged, the Method on which
+ * the listener should be messaged, and the Object array of arguments to be supplied to the
+ * Method. No attempts are made to determine whether this triplet was already added.<br>
+ *
+ * Messages are dispatched in the order in which they're added via this method; so if triplet
+ * X is added after triplet Z, then triplet Z will undergo messaging prior to triplet X.<br>
+ *
+ * This method should not be called once the owning Thread instance has been started; if it
+ * is called, the triplet will not be added to the messaging queue.<br>
+ *
+ * @param listenerInstance the instance of the listener to receive the associated notification
+ * @param listenerMethod the Method instance representing the method through which notification
+ * will occur
+ * @param methodArguments the arguments supplied to the notification method
+ */
+ public void addListenerTriplet(Object listenerInstance, Method listenerMethod,
+ Object[] methodArguments)
+ {
+ if (!this.isRunning) {
+ this.triplets.add(new TripletContainer(listenerInstance, listenerMethod,
+ methodArguments));
+ }
+ }
+
+ /**
+ * @return whether this instance has finished dispatching its messages
+ */
+ public boolean hasFinished() {
+ return this.hasFinishedDispatching;
+ }
+
+ public void run() {
+ ListIterator<TripletContainer> li = null;
+
+ this.isRunning = true;
+
+ li = this.triplets.listIterator();
+ while (li.hasNext()) {
+ TripletContainer tc = li.next();
+
+ try {
+ tc.getListenerMethod().invoke(tc.getListenerInstance(), tc.getMethodArguments());
+ } catch (Exception e) {
+ System.err.println("Exception dispatching an event: " + e);
+
+ e.printStackTrace();
+ }
+ }
+
+ this.hasFinishedDispatching = true;
+ }
+
+
+ protected class TripletContainer {
+
+ protected Object listenerInstance;
+ protected Method listenerMethod;
+ protected Object[] methodArguments;
+
+ protected TripletContainer (Object inst, Method meth, Object[] args) {
+ super();
+
+ this.listenerInstance = inst;
+ this.listenerMethod = meth;
+ this.methodArguments = args;
+ }
+
+ protected Object getListenerInstance() {
+ return this.listenerInstance;
+ }
+
+ protected Method getListenerMethod() {
+ return this.listenerMethod;
+ }
+
+ protected Object[] getMethodArguments() {
+ return this.methodArguments;
+ }
+ }
+}
\ No newline at end of file diff --git a/src/org/jivesoftware/smackx/workgroup/util/MetaDataUtils.java b/src/org/jivesoftware/smackx/workgroup/util/MetaDataUtils.java new file mode 100644 index 0000000..5be1c1a --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/util/MetaDataUtils.java @@ -0,0 +1,103 @@ +/**
+ * 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.smackx.workgroup.util;
+
+import org.jivesoftware.smackx.workgroup.MetaData;
+import org.jivesoftware.smack.util.StringUtils;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * Utility class for meta-data parsing and writing.
+ *
+ * @author Matt Tucker
+ */
+public class MetaDataUtils {
+
+ /**
+ * Parses any available meta-data and returns it as a Map of String name/value pairs. The
+ * parser must be positioned at an opening meta-data tag, or the an empty map will be returned.
+ *
+ * @param parser the XML parser positioned at an opening meta-data tag.
+ * @return the meta-data.
+ * @throws XmlPullParserException if an error occurs while parsing the XML.
+ * @throws IOException if an error occurs while parsing the XML.
+ */
+ public static Map<String, List<String>> parseMetaData(XmlPullParser parser) throws XmlPullParserException, IOException {
+ int eventType = parser.getEventType();
+
+ // If correctly positioned on an opening meta-data tag, parse meta-data.
+ if ((eventType == XmlPullParser.START_TAG)
+ && parser.getName().equals(MetaData.ELEMENT_NAME)
+ && parser.getNamespace().equals(MetaData.NAMESPACE)) {
+ Map<String, List<String>> metaData = new Hashtable<String, List<String>>();
+
+ eventType = parser.nextTag();
+
+ // Keep parsing until we've gotten to end of meta-data.
+ while ((eventType != XmlPullParser.END_TAG)
+ || (!parser.getName().equals(MetaData.ELEMENT_NAME))) {
+ String name = parser.getAttributeValue(0);
+ String value = parser.nextText();
+
+ if (metaData.containsKey(name)) {
+ List<String> values = metaData.get(name);
+ values.add(value);
+ }
+ else {
+ List<String> values = new ArrayList<String>();
+ values.add(value);
+ metaData.put(name, values);
+ }
+
+ eventType = parser.nextTag();
+ }
+
+ return metaData;
+ }
+
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Serializes a Map of String name/value pairs into the meta-data XML format.
+ *
+ * @param metaData the Map of meta-data as Map<String,List<String>>
+ * @return the meta-data values in XML form.
+ */
+ public static String serializeMetaData(Map<String, List<String>> metaData) {
+ StringBuilder buf = new StringBuilder();
+ if (metaData != null && metaData.size() > 0) {
+ buf.append("<metadata xmlns=\"http://jivesoftware.com/protocol/workgroup\">");
+ for (Iterator<String> i = metaData.keySet().iterator(); i.hasNext();) {
+ String key = i.next();
+ List<String> value = metaData.get(key);
+ for (Iterator<String> it = value.iterator(); it.hasNext();) {
+ String v = it.next();
+ buf.append("<value name=\"").append(key).append("\">");
+ buf.append(StringUtils.escapeForXML(v));
+ buf.append("</value>");
+ }
+ }
+ buf.append("</metadata>");
+ }
+ return buf.toString();
+ }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/util/ModelUtil.java b/src/org/jivesoftware/smackx/workgroup/util/ModelUtil.java new file mode 100644 index 0000000..0a4df15 --- /dev/null +++ b/src/org/jivesoftware/smackx/workgroup/util/ModelUtil.java @@ -0,0 +1,322 @@ +/**
+ * 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.smackx.workgroup.util;
+
+import java.util.*;
+
+/**
+ * Utility methods frequently used by data classes and design-time
+ * classes.
+ */
+public final class ModelUtil {
+ private ModelUtil() {
+ // Prevents instantiation.
+ }
+
+ /**
+ * This is a utility method that compares two objects when one or
+ * both of the objects might be <CODE>null</CODE> The result of
+ * this method is determined as follows:
+ * <OL>
+ * <LI>If <CODE>o1</CODE> and <CODE>o2</CODE> are the same object
+ * according to the <CODE>==</CODE> operator, return
+ * <CODE>true</CODE>.
+ * <LI>Otherwise, if either <CODE>o1</CODE> or <CODE>o2</CODE> is
+ * <CODE>null</CODE>, return <CODE>false</CODE>.
+ * <LI>Otherwise, return <CODE>o1.equals(o2)</CODE>.
+ * </OL>
+ * <p/>
+ * This method produces the exact logically inverted result as the
+ * {@link #areDifferent(Object, Object)} method.<P>
+ * <p/>
+ * For array types, one of the <CODE>equals</CODE> methods in
+ * {@link java.util.Arrays} should be used instead of this method.
+ * Note that arrays with more than one dimension will require some
+ * custom code in order to implement <CODE>equals</CODE> properly.
+ */
+ public static final boolean areEqual(Object o1, Object o2) {
+ if (o1 == o2) {
+ return true;
+ }
+ else if (o1 == null || o2 == null) {
+ return false;
+ }
+ else {
+ return o1.equals(o2);
+ }
+ }
+
+ /**
+ * This is a utility method that compares two Booleans when one or
+ * both of the objects might be <CODE>null</CODE> The result of
+ * this method is determined as follows:
+ * <OL>
+ * <LI>If <CODE>b1</CODE> and <CODE>b2</CODE> are both TRUE or
+ * neither <CODE>b1</CODE> nor <CODE>b2</CODE> is TRUE,
+ * return <CODE>true</CODE>.
+ * <LI>Otherwise, return <CODE>false</CODE>.
+ * </OL>
+ * <p/>
+ */
+ public static final boolean areBooleansEqual(Boolean b1, Boolean b2) {
+ // !jwetherb treat NULL the same as Boolean.FALSE
+ return (b1 == Boolean.TRUE && b2 == Boolean.TRUE) ||
+ (b1 != Boolean.TRUE && b2 != Boolean.TRUE);
+ }
+
+ /**
+ * This is a utility method that compares two objects when one or
+ * both of the objects might be <CODE>null</CODE>. The result
+ * returned by this method is determined as follows:
+ * <OL>
+ * <LI>If <CODE>o1</CODE> and <CODE>o2</CODE> are the same object
+ * according to the <CODE>==</CODE> operator, return
+ * <CODE>false</CODE>.
+ * <LI>Otherwise, if either <CODE>o1</CODE> or <CODE>o2</CODE> is
+ * <CODE>null</CODE>, return <CODE>true</CODE>.
+ * <LI>Otherwise, return <CODE>!o1.equals(o2)</CODE>.
+ * </OL>
+ * <p/>
+ * This method produces the exact logically inverted result as the
+ * {@link #areEqual(Object, Object)} method.<P>
+ * <p/>
+ * For array types, one of the <CODE>equals</CODE> methods in
+ * {@link java.util.Arrays} should be used instead of this method.
+ * Note that arrays with more than one dimension will require some
+ * custom code in order to implement <CODE>equals</CODE> properly.
+ */
+ public static final boolean areDifferent(Object o1, Object o2) {
+ return !areEqual(o1, o2);
+ }
+
+
+ /**
+ * This is a utility method that compares two Booleans when one or
+ * both of the objects might be <CODE>null</CODE> The result of
+ * this method is determined as follows:
+ * <OL>
+ * <LI>If <CODE>b1</CODE> and <CODE>b2</CODE> are both TRUE or
+ * neither <CODE>b1</CODE> nor <CODE>b2</CODE> is TRUE,
+ * return <CODE>false</CODE>.
+ * <LI>Otherwise, return <CODE>true</CODE>.
+ * </OL>
+ * <p/>
+ * This method produces the exact logically inverted result as the
+ * {@link #areBooleansEqual(Boolean, Boolean)} method.<P>
+ */
+ public static final boolean areBooleansDifferent(Boolean b1, Boolean b2) {
+ return !areBooleansEqual(b1, b2);
+ }
+
+
+ /**
+ * Returns <CODE>true</CODE> if the specified array is not null
+ * and contains a non-null element. Returns <CODE>false</CODE>
+ * if the array is null or if all the array elements are null.
+ */
+ public static final boolean hasNonNullElement(Object[] array) {
+ if (array != null) {
+ final int n = array.length;
+ for (int i = 0; i < n; i++) {
+ if (array[i] != null) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a single string that is the concatenation of all the
+ * strings in the specified string array. A single space is
+ * put between each string array element. Null array elements
+ * are skipped. If the array itself is null, the empty string
+ * is returned. This method is guaranteed to return a non-null
+ * value, if no expections are thrown.
+ */
+ public static final String concat(String[] strs) {
+ return concat(strs, " "); //NOTRANS
+ }
+
+ /**
+ * Returns a single string that is the concatenation of all the
+ * strings in the specified string array. The strings are separated
+ * by the specified delimiter. Null array elements are skipped. If
+ * the array itself is null, the empty string is returned. This
+ * method is guaranteed to return a non-null value, if no expections
+ * are thrown.
+ */
+ public static final String concat(String[] strs, String delim) {
+ if (strs != null) {
+ final StringBuilder buf = new StringBuilder();
+ final int n = strs.length;
+ for (int i = 0; i < n; i++) {
+ final String str = strs[i];
+ if (str != null) {
+ buf.append(str).append(delim);
+ }
+ }
+ final int length = buf.length();
+ if (length > 0) {
+ // Trim trailing space.
+ buf.setLength(length - 1);
+ }
+ return buf.toString();
+ }
+ else {
+ return ""; // NOTRANS
+ }
+ }
+
+ /**
+ * Returns <CODE>true</CODE> if the specified {@link String} is not
+ * <CODE>null</CODE> and has a length greater than zero. This is
+ * a very frequently occurring check.
+ */
+ public static final boolean hasLength(String s) {
+ return (s != null && s.length() > 0);
+ }
+
+
+ /**
+ * Returns <CODE>null</CODE> if the specified string is empty or
+ * <CODE>null</CODE>. Otherwise the string itself is returned.
+ */
+ public static final String nullifyIfEmpty(String s) {
+ return ModelUtil.hasLength(s) ? s : null;
+ }
+
+ /**
+ * Returns <CODE>null</CODE> if the specified object is null
+ * or if its <CODE>toString()</CODE> representation is empty.
+ * Otherwise, the <CODE>toString()</CODE> representation of the
+ * object itself is returned.
+ */
+ public static final String nullifyingToString(Object o) {
+ return o != null ? nullifyIfEmpty(o.toString()) : null;
+ }
+
+ /**
+ * Determines if a string has been changed.
+ *
+ * @param oldString is the initial value of the String
+ * @param newString is the new value of the String
+ * @return true If both oldString and newString are null or if they are
+ * both not null and equal to each other. Otherwise returns false.
+ */
+ public static boolean hasStringChanged(String oldString, String newString) {
+ if (oldString == null && newString == null) {
+ return false;
+ }
+ else if ((oldString == null && newString != null)
+ || (oldString != null && newString == null)) {
+ return true;
+ }
+ else {
+ return !oldString.equals(newString);
+ }
+ }
+
+ public static String getTimeFromLong(long diff) {
+ final String HOURS = "h";
+ final String MINUTES = "min";
+ final String SECONDS = "sec";
+
+ final long MS_IN_A_DAY = 1000 * 60 * 60 * 24;
+ final long MS_IN_AN_HOUR = 1000 * 60 * 60;
+ final long MS_IN_A_MINUTE = 1000 * 60;
+ final long MS_IN_A_SECOND = 1000;
+ diff = diff % MS_IN_A_DAY;
+ long numHours = diff / MS_IN_AN_HOUR;
+ diff = diff % MS_IN_AN_HOUR;
+ long numMinutes = diff / MS_IN_A_MINUTE;
+ diff = diff % MS_IN_A_MINUTE;
+ long numSeconds = diff / MS_IN_A_SECOND;
+ diff = diff % MS_IN_A_SECOND;
+
+ StringBuilder buf = new StringBuilder();
+ if (numHours > 0) {
+ buf.append(numHours + " " + HOURS + ", ");
+ }
+
+ if (numMinutes > 0) {
+ buf.append(numMinutes + " " + MINUTES + ", ");
+ }
+
+ buf.append(numSeconds + " " + SECONDS);
+
+ String result = buf.toString();
+ return result;
+ }
+
+
+ /**
+ * Build a List of all elements in an Iterator.
+ */
+ public static <T> List<T> iteratorAsList(Iterator<T> i) {
+ ArrayList<T> list = new ArrayList<T>(10);
+ while (i.hasNext()) {
+ list.add(i.next());
+ }
+ return list;
+ }
+
+ /**
+ * Creates an Iterator that is the reverse of a ListIterator.
+ */
+ public static <T> Iterator<T> reverseListIterator(ListIterator<T> i) {
+ return new ReverseListIterator<T>(i);
+ }
+}
+
+/**
+ * An Iterator that is the reverse of a ListIterator.
+ */
+class ReverseListIterator<T> implements Iterator<T> {
+ private ListIterator<T> _i;
+
+ ReverseListIterator(ListIterator<T> i) {
+ _i = i;
+ while (_i.hasNext())
+ _i.next();
+ }
+
+ public boolean hasNext() {
+ return _i.hasPrevious();
+ }
+
+ public T next() {
+ return _i.previous();
+ }
+
+ public void remove() {
+ _i.remove();
+ }
+
+}
+
+
+
+
+
+
+
+
+
+
+
+ |