From 910b012119179534a5b88c0821872ccd456c2cef Mon Sep 17 00:00:00 2001 From: rcollier Date: Thu, 28 Feb 2013 12:18:23 +0000 Subject: [PATCH 01/41] Branch for finishing up the remaining tasks for 3.3. Allowing development to continue on trunk. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3.3@13532 b35dd754-fafc-0310-a699-88a17e54d16e From 2bb4f5dffb0b47bb8300fe4d7550a7866d4eea6f Mon Sep 17 00:00:00 2001 From: rcollier Date: Thu, 28 Feb 2013 12:22:38 +0000 Subject: [PATCH 02/41] git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3@13533 b35dd754-fafc-0310-a699-88a17e54d16e From a5431f2ee8c8302e0df61ec537d972ff873994c3 Mon Sep 17 00:00:00 2001 From: rcollier Date: Thu, 28 Feb 2013 12:22:59 +0000 Subject: [PATCH 03/41] git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13534 b35dd754-fafc-0310-a699-88a17e54d16e From 1cdb86989adf8c79854e8e6d440a4bf729fbd956 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Tue, 5 Mar 2013 10:35:04 +0000 Subject: [PATCH 04/41] SMACK-423 Parse unhandled IQ stanzas of type 'request' to dummy IQ class, so that the contents can be examined later on. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13539 b35dd754-fafc-0310-a699-88a17e54d16e --- .../smack/util/PacketParserUtils.java | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/source/org/jivesoftware/smack/util/PacketParserUtils.java b/source/org/jivesoftware/smack/util/PacketParserUtils.java index 14d5d47ab..a574be3db 100644 --- a/source/org/jivesoftware/smack/util/PacketParserUtils.java +++ b/source/org/jivesoftware/smack/util/PacketParserUtils.java @@ -171,13 +171,13 @@ public class PacketParserUtils { */ private static String parseContent(XmlPullParser parser) throws XmlPullParserException, IOException { - String content = ""; + StringBuffer content = new StringBuffer(); int parserDepth = parser.getDepth(); while (!(parser.next() == XmlPullParser.END_TAG && parser .getDepth() == parserDepth)) { - content += parser.getText(); + content.append(parser.getText()); } - return content; + return content.toString(); } /** @@ -325,6 +325,13 @@ public class PacketParserUtils { (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) { @@ -340,6 +347,7 @@ public class PacketParserUtils { // 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; } @@ -355,6 +363,7 @@ public class PacketParserUtils { else { // If an IQ packet wasn't created above, create an empty IQ packet. iqPacket = new IQ() { + @Override public String getChildElementXML() { return null; } @@ -854,7 +863,7 @@ public class PacketParserUtils { } } return object; - } + } /** * Decodes a String into an object of the specified type. If the object @@ -889,4 +898,24 @@ public class PacketParserUtils { } 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; + } + } } From 21be8c55eecde82615ebe43c0f798e52a5fe8167 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 18 Mar 2013 08:50:48 +0000 Subject: [PATCH 05/41] SMACK-361 Added support for Entity Capabilities. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13560 b35dd754-fafc-0310-a699-88a17e54d16e --- build/resources/META-INF/smack-config.xml | 5 +- build/resources/META-INF/smack.providers | 7 + source/org/jivesoftware/smack/Connection.java | 27 + .../org/jivesoftware/smack/PacketReader.java | 13 + .../smack/SmackConfiguration.java | 25 + source/org/jivesoftware/smack/packet/IQ.java | 8 + .../org/jivesoftware/smack/packet/Packet.java | 33 +- .../smack/util/Base32Encoder.java | 187 +++++ .../smack/util/Base64Encoder.java | 44 ++ .../smack/util/StringEncoder.java | 38 + source/org/jivesoftware/smackx/Form.java | 11 +- source/org/jivesoftware/smackx/FormField.java | 42 ++ .../smackx/NodeInformationProvider.java | 9 +- .../smackx/ServiceDiscoveryManager.java | 238 ++++-- .../smackx/commands/AdHocCommandManager.java | 14 +- .../smackx/entitycaps/EntityCapsManager.java | 713 ++++++++++++++++++ .../cache/EntityCapsPersistentCache.java | 38 + .../cache/SimpleDirectoryPersistentCache.java | 193 +++++ .../entitycaps/packet/CapsExtension.java | 83 ++ .../provider/CapsExtensionProvider.java | 60 ++ .../smackx/muc/MultiUserChat.java | 6 + .../jivesoftware/smackx/packet/DataForm.java | 20 +- .../smackx/packet/DiscoverInfo.java | 205 ++++- .../smackx/packet/DiscoverItems.java | 13 + .../smackx/provider/DiscoverInfoProvider.java | 7 +- .../socks5/Socks5ByteStreamManagerTest.java | 23 +- .../entitycaps/EntityCapsManagerTest.java | 227 ++++++ .../smackx/pubsub/ConfigureFormTest.java | 6 +- .../jivesoftware/smack/ReconnectionTest.java | 2 +- .../smack/test/SmackTestCase.java | 10 + .../smack/util/ConnectionUtils.java | 27 + .../smackx/ServiceDiscoveryManagerTest.java | 2 +- .../smackx/entitycaps/EntityCapsTest.java | 147 ++++ 33 files changed, 2395 insertions(+), 88 deletions(-) create mode 100644 source/org/jivesoftware/smack/util/Base32Encoder.java create mode 100644 source/org/jivesoftware/smack/util/Base64Encoder.java create mode 100644 source/org/jivesoftware/smack/util/StringEncoder.java create mode 100644 source/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java create mode 100644 source/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java create mode 100644 source/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java create mode 100644 source/org/jivesoftware/smackx/entitycaps/packet/CapsExtension.java create mode 100644 source/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java create mode 100644 test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java create mode 100644 test/org/jivesoftware/smack/util/ConnectionUtils.java create mode 100644 test/org/jivesoftware/smackx/entitycaps/EntityCapsTest.java diff --git a/build/resources/META-INF/smack-config.xml b/build/resources/META-INF/smack-config.xml index c4e99936a..6c4b891e2 100644 --- a/build/resources/META-INF/smack-config.xml +++ b/build/resources/META-INF/smack-config.xml @@ -30,8 +30,11 @@ 10000 - + 1800 + + false + diff --git a/build/resources/META-INF/smack.providers b/build/resources/META-INF/smack.providers index 0de71aa04..f1a7c1f91 100644 --- a/build/resources/META-INF/smack.providers +++ b/build/resources/META-INF/smack.providers @@ -678,4 +678,11 @@ urn:xmpp:receipts org.jivesoftware.smackx.receipts.DeliveryReceiptRequest$Provider + + + + c + http://jabber.org/protocol/caps + org.jivesoftware.smackx.entitycaps.provider.CapsExtensionProvider + diff --git a/source/org/jivesoftware/smack/Connection.java b/source/org/jivesoftware/smack/Connection.java index d041067a4..9a213dbc8 100644 --- a/source/org/jivesoftware/smack/Connection.java +++ b/source/org/jivesoftware/smack/Connection.java @@ -204,6 +204,11 @@ public abstract class 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; /** @@ -795,7 +800,29 @@ public abstract class Connection { } } + /** + * 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. diff --git a/source/org/jivesoftware/smack/PacketReader.java b/source/org/jivesoftware/smack/PacketReader.java index 616a18dbe..590dfd951 100644 --- a/source/org/jivesoftware/smack/PacketReader.java +++ b/source/org/jivesoftware/smack/PacketReader.java @@ -393,6 +393,19 @@ class PacketReader { // The server requires the client to bind a resource to the stream connection.getSASLAuthentication().bindingRequired(); } + // 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(); diff --git a/source/org/jivesoftware/smack/SmackConfiguration.java b/source/org/jivesoftware/smack/SmackConfiguration.java index 83f8d22f3..80f1906af 100644 --- a/source/org/jivesoftware/smack/SmackConfiguration.java +++ b/source/org/jivesoftware/smack/SmackConfiguration.java @@ -63,6 +63,11 @@ public final class SmackConfiguration { */ 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() { } @@ -115,6 +120,9 @@ public final class SmackConfiguration { else if (parser.getName().equals("defaultPingInterval")) { defaultPingInterval = parseIntProperty(parser, defaultPingInterval); } + else if (parser.getName().equals("autoEnableEntityCaps")) { + autoEnableEntityCaps = Boolean.parseBoolean(parser.nextText()); + } } eventType = parser.next(); } @@ -329,6 +337,23 @@ public final class SmackConfiguration { 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 diff --git a/source/org/jivesoftware/smack/packet/IQ.java b/source/org/jivesoftware/smack/packet/IQ.java index 8b844674c..8e1f7d4ab 100644 --- a/source/org/jivesoftware/smack/packet/IQ.java +++ b/source/org/jivesoftware/smack/packet/IQ.java @@ -43,6 +43,14 @@ 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. * diff --git a/source/org/jivesoftware/smack/packet/Packet.java b/source/org/jivesoftware/smack/packet/Packet.java index 883462b1b..041d8c892 100644 --- a/source/org/jivesoftware/smack/packet/Packet.java +++ b/source/org/jivesoftware/smack/packet/Packet.java @@ -90,6 +90,22 @@ public abstract class Packet { private final Map properties = new HashMap(); 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 null when * ID_NOT_AVAILABLE was set as the packet's id. @@ -247,14 +263,25 @@ public abstract class Packet { } /** - * Adds a packet extension to the packet. + * 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 extensions) { + if (extensions == null) return; + packetExtensions.addAll(extensions); + } + /** * Removes a packet extension from the packet. * @@ -266,7 +293,7 @@ public abstract class Packet { /** * Returns the packet property with the specified name or null if the - * property doesn't exist. Property values that were orginally primitives will + * 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. * @@ -456,4 +483,4 @@ public abstract class Packet { result = 31 * result + (error != null ? error.hashCode() : 0); return result; } -} \ No newline at end of file +} diff --git a/source/org/jivesoftware/smack/util/Base32Encoder.java b/source/org/jivesoftware/smack/util/Base32Encoder.java new file mode 100644 index 000000000..c7cc1d028 --- /dev/null +++ b/source/org/jivesoftware/smack/util/Base32Encoder.java @@ -0,0 +1,187 @@ +/** + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF 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 Base32 Wikipedia entry + * + */ +public class Base32Encoder implements StringEncoder { + + private static Base32Encoder instance; + private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ2345678"; + + private Base32Encoder() { + // Use getInstance() + } + + public static Base32Encoder getInstance() { + if (instance == null) { + instance = new Base32Encoder(); + } + 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/source/org/jivesoftware/smack/util/Base64Encoder.java b/source/org/jivesoftware/smack/util/Base64Encoder.java new file mode 100644 index 000000000..78399b463 --- /dev/null +++ b/source/org/jivesoftware/smack/util/Base64Encoder.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.smack.util; + + +/** + * @author Florian Schmaus + */ +public class Base64Encoder implements StringEncoder { + + private static Base64Encoder instance; + + private Base64Encoder() { + // Use getInstance() + } + + public static Base64Encoder getInstance() { + if (instance == null) { + instance = new Base64Encoder(); + } + 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/source/org/jivesoftware/smack/util/StringEncoder.java b/source/org/jivesoftware/smack/util/StringEncoder.java new file mode 100644 index 000000000..5a15c9548 --- /dev/null +++ b/source/org/jivesoftware/smack/util/StringEncoder.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. + */ + +/** + * @author Florian Schmaus + */ +package org.jivesoftware.smack.util; + +// TODO move StringEncoder, Base64Encoder and Base32Encoder to smack.util + +public interface StringEncoder { + /** + * Encodes an string to another representation + * + * @param string + * @return + */ + public String encode(String string); + + /** + * Decodes an string back to it's initial representation + * + * @param string + * @return + */ + public String decode(String string); +} diff --git a/source/org/jivesoftware/smackx/Form.java b/source/org/jivesoftware/smackx/Form.java index 8b654ab74..992c03619 100644 --- a/source/org/jivesoftware/smackx/Form.java +++ b/source/org/jivesoftware/smackx/Form.java @@ -42,17 +42,22 @@ import org.jivesoftware.smackx.packet.DataForm; * 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 XEP-0004 Data Forms + * * @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". diff --git a/source/org/jivesoftware/smackx/FormField.java b/source/org/jivesoftware/smackx/FormField.java index a10196032..44dfa8cec 100644 --- a/source/org/jivesoftware/smackx/FormField.java +++ b/source/org/jivesoftware/smackx/FormField.java @@ -299,6 +299,26 @@ public class FormField { return buf.toString(); } + public boolean equals(Object obj) { + if (obj == null) + return false; + if (obj == this) + return true; + if (obj.getClass() != getClass()) + return false; + + FormField other = (FormField) obj; + + String thisXml = toXML(); + String otherXml = other.toXML(); + + if (thisXml.equals(otherXml)) { + return true; + } else { + return false; + } + } + /** * Represents the available option of a given FormField. * @@ -354,5 +374,27 @@ public class FormField { buf.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; + + 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; + } } } diff --git a/source/org/jivesoftware/smackx/NodeInformationProvider.java b/source/org/jivesoftware/smackx/NodeInformationProvider.java index 816be4dba..68bb613d3 100644 --- a/source/org/jivesoftware/smackx/NodeInformationProvider.java +++ b/source/org/jivesoftware/smackx/NodeInformationProvider.java @@ -20,6 +20,7 @@ package org.jivesoftware.smackx; +import org.jivesoftware.smack.packet.PacketExtension; import org.jivesoftware.smackx.packet.DiscoverInfo; import org.jivesoftware.smackx.packet.DiscoverItems; @@ -36,7 +37,7 @@ import java.util.List; * @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 @@ -65,4 +66,10 @@ public interface NodeInformationProvider { */ public abstract List getNodeIdentities(); + /** + * Returns a list of the packet extensions defined in the node. + * + * @return a list of the packet extensions defined in the node. + */ + public abstract List getNodePacketExtensions(); } diff --git a/source/org/jivesoftware/smackx/ServiceDiscoveryManager.java b/source/org/jivesoftware/smackx/ServiceDiscoveryManager.java index 8e184b765..f0e7912aa 100644 --- a/source/org/jivesoftware/smackx/ServiceDiscoveryManager.java +++ b/source/org/jivesoftware/smackx/ServiceDiscoveryManager.java @@ -26,8 +26,11 @@ 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; @@ -47,8 +50,13 @@ import java.util.concurrent.ConcurrentHashMap; */ public class ServiceDiscoveryManager { - private static String identityName = "Smack"; - private static String identityType = "pc"; + 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 identities = new LinkedList(); + + private EntityCapsManager capsManager; private static Map instances = new ConcurrentHashMap(); @@ -66,6 +74,7 @@ public class ServiceDiscoveryManager { new ServiceDiscoveryManager(connection); } }); + identities.add(new Identity(DEFAULT_IDENTITY_CATEGORY, DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE)); } /** @@ -77,6 +86,7 @@ public class ServiceDiscoveryManager { */ public ServiceDiscoveryManager(Connection connection) { this.connection = connection; + init(); } @@ -98,7 +108,12 @@ public class ServiceDiscoveryManager { * in a disco request. */ public static String getIdentityName() { - return identityName; + DiscoverInfo.Identity identity = identities.get(0); + if (identity != null) { + return identity.getName(); + } else { + return null; + } } /** @@ -109,7 +124,9 @@ public class ServiceDiscoveryManager { * in a disco request. */ public static void setIdentityName(String name) { - identityName = name; + DiscoverInfo.Identity identity = identities.remove(0); + identity = new DiscoverInfo.Identity(DEFAULT_IDENTITY_CATEGORY, name, DEFAULT_IDENTITY_TYPE); + identities.add(identity); } /** @@ -121,7 +138,12 @@ public class ServiceDiscoveryManager { * disco request. */ public static String getIdentityType() { - return identityType; + DiscoverInfo.Identity identity = identities.get(0); + if (identity != null) { + return identity.getType(); + } else { + return null; + } } /** @@ -133,7 +155,22 @@ public class ServiceDiscoveryManager { * disco request. */ public static void setIdentityType(String type) { - identityType = 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 getIdentities() { + return Collections.unmodifiableList(identities); } /** @@ -190,13 +227,10 @@ public class ServiceDiscoveryManager { NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverItems.getNode()); if (nodeInformationProvider != null) { - // Specified node was found - List items = nodeInformationProvider.getNodeItems(); - if (items != null) { - for (DiscoverItems.Item item : items) { - response.addItem(item); - } - } + // Specified node was found, add node items + response.addItems(nodeInformationProvider.getNodeItems()); + // Add packet extensions + response.addExtensions(nodeInformationProvider.getNodePacketExtensions()); } else if(discoverItems.getNode() != null) { // Return error since client doesn't contain // the specified node @@ -222,22 +256,12 @@ public class ServiceDiscoveryManager { response.setTo(discoverInfo.getFrom()); response.setPacketID(discoverInfo.getPacketID()); response.setNode(discoverInfo.getNode()); - // Add the client's identity and features only if "node" is null + // 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) { - // Set this client identity - DiscoverInfo.Identity identity = new DiscoverInfo.Identity("client", - getIdentityName()); - identity.setType(getIdentityType()); - response.addIdentity(identity); - // Add the registered features to the response - synchronized (features) { - for (Iterator it = getFeatures(); it.hasNext();) { - response.addFeature(it.next()); - } - if (extendedInfo != null) { - response.addExtension(extendedInfo); - } - } + addDiscoverInfoTo(response); } else { // Disco#info was sent to a node. Check if we have information of the @@ -246,20 +270,11 @@ public class ServiceDiscoveryManager { getNodeInformationProvider(discoverInfo.getNode()); if (nodeInformationProvider != null) { // Node was found. Add node features - List features = nodeInformationProvider.getNodeFeatures(); - if (features != null) { - for(String feature : features) { - response.addFeature(feature); - } - } + response.addFeatures(nodeInformationProvider.getNodeFeatures()); // Add node identities - List identities = - nodeInformationProvider.getNodeIdentities(); - if (identities != null) { - for (DiscoverInfo.Identity identity : identities) { - response.addIdentity(identity); - } - } + response.addIdentities(nodeInformationProvider.getNodeIdentities()); + // Add packet extensions + response.addExtensions(nodeInformationProvider.getNodePacketExtensions()); } else { // Return error since specified node was not found @@ -274,6 +289,26 @@ public class ServiceDiscoveryManager { connection.addPacketListener(packetListener, packetFilter); } + /** + * Add discover info response data. + * + * @see XEP-30 Basic Protocol; Example 2 + * + * @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 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 null if none.

@@ -334,6 +369,17 @@ public class ServiceDiscoveryManager { } } + /** + * Returns the supported features by this XMPP entity. + * + * @return a copy of the List on the supported features by this XMPP entity. + */ + public List getFeaturesList() { + synchronized (features) { + return new LinkedList(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.

@@ -348,6 +394,7 @@ public class ServiceDiscoveryManager { public void addFeature(String feature) { synchronized (features) { features.add(feature); + renewEntityCapsVersion(); } } @@ -362,6 +409,7 @@ public class ServiceDiscoveryManager { public void removeFeature(String feature) { synchronized (features) { features.remove(feature); + renewEntityCapsVersion(); } } @@ -394,10 +442,36 @@ public class ServiceDiscoveryManager { */ public void setExtendedInfo(DataForm info) { extendedInfo = info; + renewEntityCapsVersion(); } /** - * Removes the dataform containing extended service discovery information + * Returns the data form that is set as extended information for this Service Discovery instance (XEP-0128) + * + * @see XEP-128: Service Discovery Extensions + * @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 getExtendedInfoAsList() { + List res = null; + if (extendedInfo != null) { + res = new ArrayList(1); + res.add(extendedInfo); + } + return res; + } + + /** + * Removes the data form containing extended service discovery information * from the information returned by this XMPP entity.

* * Since no packet is actually sent to the server it is safe to perform this @@ -405,17 +479,45 @@ public class ServiceDiscoveryManager { */ 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. + * @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 { - return discoverInfo(entityID, null); + 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.verifyDiscvoerInfoVersion(nvh.getVer(), nvh.getHash(), info)) + EntityCapsManager.addDiscoverInfoByNode(nvh.getNodeVer(), info); + } + + return info; } /** @@ -423,8 +525,11 @@ public class ServiceDiscoveryManager { * note attribute. Use this message only when trying to query information which is not * directly addressable. * + * @see XEP-30 Basic Protocol + * @see XEP-30 Info Nodes + * * @param entityID the address of the XMPP entity. - * @param node the attribute that supplements the 'jid' attribute. + * @param node the optional attribute that supplements the 'jid' attribute. * @return the discovered information. * @throws XMPPException if the operation failed for some reason. */ @@ -471,7 +576,7 @@ public class ServiceDiscoveryManager { * directly addressable. * * @param entityID the address of the XMPP entity. - * @param node the attribute that supplements the 'jid' attribute. + * @param node the optional attribute that supplements the 'jid' attribute. * @return the discovered items. * @throws XMPPException if the operation failed for some reason. */ @@ -513,8 +618,21 @@ public class ServiceDiscoveryManager { */ public boolean canPublishItems(String entityID) throws XMPPException { DiscoverInfo info = discoverInfo(entityID); - return info.containsFeature("http://jabber.org/protocol/disco#publish"); - } + 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 @@ -565,4 +683,26 @@ public class ServiceDiscoveryManager { throw new XMPPException(result.getError()); } } -} \ No newline at end of file + + /** + * 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/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java b/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java index d80c1ac63..f32c48ec2 100755 --- a/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java +++ b/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java @@ -25,6 +25,7 @@ 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; @@ -181,12 +182,16 @@ public class AdHocCommandManager { public List getNodeIdentities() { List answer = new ArrayList(); DiscoverInfo.Identity identity = new DiscoverInfo.Identity( - "automation", name); - identity.setType("command-node"); + "automation", name, "command-node"); answer.add(identity); return answer; } + @Override + public List getNodePacketExtensions() { + return null; + } + }); } @@ -319,6 +324,11 @@ public class AdHocCommandManager { public List getNodeIdentities() { return null; } + + @Override + public List getNodePacketExtensions() { + return null; + } }); // The packet listener and the filter for processing some AdHoc Commands diff --git a/source/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java b/source/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java new file mode 100644 index 000000000..d5d6402d2 --- /dev/null +++ b/source/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 SUPPORTED_HASHES = new HashMap(); + + protected static EntityCapsPersistentCache persistentCache; + + private static Map instances = Collections + .synchronizedMap(new WeakHashMap()); + + /** + * Map of (node + '#" + hash algorithm) to DiscoverInfo data + */ + protected static Map caps = new Cache(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 jidCaps = new Cache(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 weakRefConnection; + private ServiceDiscoveryManager sdm; + private boolean entityCapsEnabled; + private String currentCapsVersion; + private boolean presenceSend = false; + private Queue lastLocalCapsVersions = new ConcurrentLinkedQueue(); + + /** + * 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); + 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 features = sdm.getFeaturesList(); + List identities = new LinkedList(ServiceDiscoveryManager.getIdentities()); + List packetExtensions = sdm.getExtendedInfoAsList(); + + @Override + public List getNodeItems() { + return null; + } + + @Override + public List getNodeFeatures() { + return features; + } + + @Override + public List getNodeIdentities() { + return identities; + } + + @Override + public List getNodePacketExtensions() { + return packetExtensions; + } + }); + + // Send an empty presence, and let the packet intercepter + // add a 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 XEP-0115 + * 5.4 Processing Method + * + * @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 verifyDiscvoerInfoVersion(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 foundFormTypes = new LinkedList(); + for (Iterator i = info.getExtensions().iterator(); i.hasNext();) { + PacketExtension pe = i.next(); + if (pe.getNamespace().equals(Form.NAMESPACE)) { + DataForm df = (DataForm) pe; + for (Iterator 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 XEP-115 + * Verification String + * + * @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 sortedIdentities = new TreeSet(); + ; + for (Iterator 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 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 features = new TreeSet(); + for (Iterator 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.hasHiddenFromTypeField()) { + 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 element). + SortedSet fs = new TreeSet(new Comparator() { + public int compare(FormField f1, FormField f2) { + return f1.getVariable().compareTo(f2.getVariable()); + } + }); + + FormField ft = null; + + for (Iterator 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 + // element. + // 3. For each 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 i, StringBuilder sb) { + SortedSet fvs = new TreeSet(); + 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/source/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java b/source/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java new file mode 100644 index 000000000..4247e7b1b --- /dev/null +++ b/source/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 + */ + abstract void addDiscoverInfoByNodePersistent(String node, DiscoverInfo info); + + /** + * Replay the Caches data into EntityCapsManager + */ + abstract void replay() throws IOException; + + /** + * Empty the Cache + */ + abstract void emptyCache(); +} diff --git a/source/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java b/source/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java new file mode 100644 index 000000000..329e4dce6 --- /dev/null +++ b/source/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java @@ -0,0 +1,193 @@ +/** + * 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.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.mxp1.MXParser; +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 stringEncoder; + + /** + * 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 Base32. + * + * @param cacheDir + */ + public SimpleDirectoryPersistentCache(File cacheDir) { + this(cacheDir, Base64Encoder.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 Base32. + * + * @param cacheDir + * @param stringEncoder + */ + public SimpleDirectoryPersistentCache(File cacheDir, StringEncoder stringEncoder) { + 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.stringEncoder = stringEncoder; + } + + @Override + public void addDiscoverInfoByNodePersistent(String node, DiscoverInfo info) { + String filename = stringEncoder.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 = stringEncoder.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 = new MXParser(); + 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/source/org/jivesoftware/smackx/entitycaps/packet/CapsExtension.java b/source/org/jivesoftware/smackx/entitycaps/packet/CapsExtension.java new file mode 100644 index 000000000..a87c86c96 --- /dev/null +++ b/source/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; + } + + /* + * + * + */ + public String toXML() { + String xml = "<" + EntityCapsManager.ELEMENT + " xmlns=\"" + EntityCapsManager.NAMESPACE + "\" " + + "hash=\"" + hash + "\" " + + "node=\"" + node + "\" " + + "ver=\"" + ver + "\"/>"; + + return xml; + } +} diff --git a/source/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java b/source/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java new file mode 100644 index 000000000..4328d21b3 --- /dev/null +++ b/source/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/source/org/jivesoftware/smackx/muc/MultiUserChat.java b/source/org/jivesoftware/smackx/muc/MultiUserChat.java index 2caa970a1..e0368028d 100644 --- a/source/org/jivesoftware/smackx/muc/MultiUserChat.java +++ b/source/org/jivesoftware/smackx/muc/MultiUserChat.java @@ -52,6 +52,7 @@ 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; @@ -133,6 +134,11 @@ public class MultiUserChat { public List getNodeIdentities() { return null; } + + @Override + public List getNodePacketExtensions() { + return null; + } }); } }); diff --git a/source/org/jivesoftware/smackx/packet/DataForm.java b/source/org/jivesoftware/smackx/packet/DataForm.java index 8fe43070f..4bc1f6994 100644 --- a/source/org/jivesoftware/smackx/packet/DataForm.java +++ b/source/org/jivesoftware/smackx/packet/DataForm.java @@ -21,6 +21,7 @@ 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; @@ -123,11 +124,11 @@ public class DataForm implements PacketExtension { } public String getElementName() { - return "x"; + return Form.ELEMENT; } public String getNamespace() { - return "jabber:x:data"; + return Form.NAMESPACE; } /** @@ -195,6 +196,21 @@ public class DataForm implements PacketExtension { } } + /** + * 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 hasHiddenFromTypeField() { + 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( diff --git a/source/org/jivesoftware/smackx/packet/DiscoverInfo.java b/source/org/jivesoftware/smackx/packet/DiscoverInfo.java index 4f4597d67..e2219032d 100644 --- a/source/org/jivesoftware/smackx/packet/DiscoverInfo.java +++ b/source/org/jivesoftware/smackx/packet/DiscoverInfo.java @@ -23,8 +23,10 @@ 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; @@ -45,6 +47,36 @@ public class DiscoverInfo extends IQ { private final List identities = new CopyOnWriteArrayList(); 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. * @@ -54,6 +86,18 @@ public class DiscoverInfo extends IQ { addFeature(new Feature(feature)); } + /** + * Adds a collection of features to the packet. Does noting if featuresToAdd is null. + * + * @param featuresToAdd + */ + public void addFeatures(Collection featuresToAdd) { + if (featuresToAdd == null) return; + for (String feature : featuresToAdd) { + addFeature(feature); + } + } + private void addFeature(Feature feature) { synchronized (features) { features.add(feature); @@ -82,6 +126,18 @@ public class DiscoverInfo extends IQ { } } + /** + * Adds identities to the DiscoverInfo stanza + * + * @param identitiesToAdd + */ + public void addIdentities(Collection identitiesToAdd) { + if (identitiesToAdd == null) return; + synchronized (identities) { + identities.addAll(identitiesToAdd); + } + } + /** * Returns the discovered identities of an XMPP entity. * @@ -158,6 +214,40 @@ public class DiscoverInfo extends IQ { return buf.toString(); } + /** + * Test if a DiscoverInfo response contains duplicate identities. + * + * @return true if duplicate identities where found, otherwise false + */ + public boolean containsDuplicateIdentities() { + List checkedIdentities = new LinkedList(); + 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 checkedFeatures = new LinkedList(); + 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.

@@ -167,21 +257,26 @@ public class DiscoverInfo extends IQ { * attributes. * */ - public static class Identity { + public static class Identity implements Comparable { private String category; private String name; private String type; + private String lang; // 'xml:lang; /** * Creates a new identity for an XMPP entity. + * 'category' and 'type' are required by + * XEP-30 XML Schemas * - * @param category the entity's category. + * @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) { + public Identity(String category, String name, String type) { this.category = category; this.name = name; + this.type = type; } /** @@ -223,16 +318,106 @@ public class DiscoverInfo extends IQ { 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(""); return buf.toString(); } + + /** + * Check equality for Identity for category, type, lang and name + * in that order as defined by + * XEP-0015 5.4 Processing Method (Step 3.3) + * + */ + 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 (!other.type.equals(type)) + return false; + if (!otherLang.equals(thisLang)) + return false; + + String otherName = other.name == null ? "" : other.name; + String thisName = name == null ? "" : other.name; + if (!thisName.equals(otherName)) + return false; + + return true; + } + + /** + * Compares and identity with another object. 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(Object obj) { + + DiscoverInfo.Identity other = (DiscoverInfo.Identity) obj; + String otherLang = other.lang == null ? "" : other.lang; + String thisLang = lang == null ? "" : lang; + + if (category.equals(other.category)) { + if (type.equals(other.type)) { + 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 type.compareTo(other.type); + } + } else { + return category.compareTo(other.category); + } + } } /** @@ -268,5 +453,17 @@ public class DiscoverInfo extends IQ { buf.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); + } } } diff --git a/source/org/jivesoftware/smackx/packet/DiscoverItems.java b/source/org/jivesoftware/smackx/packet/DiscoverItems.java index f6b6dcae1..07185e68a 100644 --- a/source/org/jivesoftware/smackx/packet/DiscoverItems.java +++ b/source/org/jivesoftware/smackx/packet/DiscoverItems.java @@ -23,6 +23,7 @@ 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; @@ -55,6 +56,18 @@ public class DiscoverItems extends IQ { } } + /** + * Adds a collection of items to the discovered information. Does nothing if itemsToAdd is null + * + * @param itemsToAdd + */ + public void addItems(Collection itemsToAdd) { + if (itemsToAdd == null) return; + for (Item i : itemsToAdd) { + addItem(i); + } + } + /** * Returns the discovered items of the queried XMPP entity. * diff --git a/source/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java b/source/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java index cbed6208f..d10049160 100644 --- a/source/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java +++ b/source/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java @@ -42,6 +42,7 @@ public class DiscoverInfoProvider implements IQProvider { String name = ""; String type = ""; String variable = ""; + String lang = ""; discoverInfo.setNode(parser.getAttributeValue("", "node")); while (!done) { int eventType = parser.next(); @@ -51,6 +52,7 @@ public class DiscoverInfoProvider implements IQProvider { 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 @@ -64,8 +66,9 @@ public class DiscoverInfoProvider implements IQProvider { } else if (eventType == XmlPullParser.END_TAG) { if (parser.getName().equals("identity")) { // Create a new identity and add it to the discovered info. - identity = new DiscoverInfo.Identity(category, name); - identity.setType(type); + identity = new DiscoverInfo.Identity(category, name, type); + if (lang != null) + identity.setLanguage(lang); discoverInfo.addIdentity(identity); } if (parser.getName().equals("feature")) { diff --git a/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java index 0a769c27c..5c7d9493c 100644 --- a/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java +++ b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java @@ -247,8 +247,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about NOT being a Socks5 // proxy DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity = new Identity("noproxy", proxyJID); - identity.setType("bytestreams"); + Identity identity = new Identity("noproxy", proxyJID, "bytestreams"); proxyInfo.addIdentity(identity); // return the proxy identity if proxy is queried @@ -312,8 +311,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about NOT being a Socks5 // proxy DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity = new Identity("noproxy", proxyJID); - identity.setType("bytestreams"); + Identity identity = new Identity("noproxy", proxyJID, "bytestreams"); proxyInfo.addIdentity(identity); // return the proxy identity if proxy is queried @@ -403,8 +401,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about being a SOCKS5 proxy DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity = new Identity("proxy", proxyJID); - identity.setType("bytestreams"); + Identity identity = new Identity("proxy", proxyJID, "bytestreams"); proxyInfo.addIdentity(identity); // return the socks5 bytestream proxy identity if proxy is queried @@ -494,8 +491,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about being a SOCKS5 proxy DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity = new Identity("proxy", proxyJID); - identity.setType("bytestreams"); + Identity identity = new Identity("proxy", proxyJID, "bytestreams"); proxyInfo.addIdentity(identity); // return the socks5 bytestream proxy identity if proxy is queried @@ -577,8 +573,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about being a SOCKS5 proxy DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity = new Identity("proxy", proxyJID); - identity.setType("bytestreams"); + Identity identity = new Identity("proxy", proxyJID, "bytestreams"); proxyInfo.addIdentity(identity); // return the socks5 bytestream proxy identity if proxy is queried @@ -672,8 +667,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about being a SOCKS5 proxy DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity = new Identity("proxy", proxyJID); - identity.setType("bytestreams"); + Identity identity = new Identity("proxy", proxyJID, "bytestreams"); proxyInfo.addIdentity(identity); // return the socks5 bytestream proxy identity if proxy is queried @@ -1026,7 +1020,7 @@ public class Socks5ByteStreamManagerTest { */ DiscoverInfo proxyInfo1 = Socks5PacketUtils.createDiscoverInfo("proxy2.xmpp-server", initiatorJID); - Identity identity1 = new Identity("proxy", "proxy2.xmpp-server"); + Identity identity1 = new Identity("proxy", "proxy2.xmpp-server", "bytestreams"); identity1.setType("bytestreams"); proxyInfo1.addIdentity(identity1); @@ -1036,8 +1030,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about being a SOCKS5 proxy DiscoverInfo proxyInfo2 = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity2 = new Identity("proxy", proxyJID); - identity2.setType("bytestreams"); + Identity identity2 = new Identity("proxy", proxyJID, "bytestreams"); proxyInfo2.addIdentity(identity2); // return the SOCKS5 bytestream proxy identity if proxy is queried diff --git a/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java b/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java new file mode 100644 index 000000000..754bf6d3c --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java @@ -0,0 +1,227 @@ +package org.jivesoftware.smackx.entitycaps; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.LinkedList; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.util.Base32Encoder; +import org.jivesoftware.smack.util.Base64Encoder; +import org.jivesoftware.smack.util.StringEncoder; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.entitycaps.EntityCapsManager; +import org.jivesoftware.smackx.entitycaps.cache.EntityCapsPersistentCache; +import org.jivesoftware.smackx.entitycaps.cache.SimpleDirectoryPersistentCache; +import org.jivesoftware.smackx.packet.DataForm; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.junit.Test; + + +public class EntityCapsManagerTest { + + /** + * XEP- + * 0115 Complex Generation Example + */ + @Test + public void testComplexGenerationExample() { + DiscoverInfo di = createComplexSamplePacket(); + + String ver = EntityCapsManager.generateVerificationString(di, "sha-1"); + assertEquals("q07IKJEyjvHSyhy//CH0CxmKi8w=", ver); + } + + @Test + public void testSimpleDirectoryCacheBase64() throws IOException { + EntityCapsManager.persistentCache = null; + testSimpleDirectoryCache(Base64Encoder.getInstance()); + } + + @Test + public void testSimpleDirectoryCacheBase32() throws IOException { + EntityCapsManager.persistentCache = null; + testSimpleDirectoryCache(Base32Encoder.getInstance()); + } + + @Test + public void testVerificationDuplicateFeatures() { + DiscoverInfo di = createMalformedDiscoverInfo(); + assertTrue(di.containsDuplicateFeatures()); + } + + @Test + public void testVerificationDuplicateIdentities() { + DiscoverInfo di = createMalformedDiscoverInfo(); + assertTrue(di.containsDuplicateIdentities()); + } + + @Test + public void testVerificationDuplicateDataForm() { + DiscoverInfo di = createMalformedDiscoverInfo(); + assertTrue(EntityCapsManager.verifyPacketExtensions(di)); + } + + private void testSimpleDirectoryCache(StringEncoder stringEncoder) throws IOException { + + EntityCapsPersistentCache cache = new SimpleDirectoryPersistentCache(createTempDirectory()); + EntityCapsManager.setPersistentCache(cache); + + DiscoverInfo di = createComplexSamplePacket(); + String nodeVer = di.getNode() + "#" + EntityCapsManager.generateVerificationString(di, "sha-1"); + + // Save the data in EntityCapsManager + EntityCapsManager.addDiscoverInfoByNode(nodeVer, di); + + // Lose all the data + EntityCapsManager.caps.clear(); + + // Restore the data from the persistent Cache + cache.replay(); + + DiscoverInfo restored_di = EntityCapsManager.getDiscoveryInfoByNodeVer(nodeVer); + assertNotNull(restored_di); + assertEquals(di.toXML(), restored_di.toXML()); + } + + private static DiscoverInfo createComplexSamplePacket() { + DiscoverInfo di = new DiscoverInfo(); + di.setFrom("benvolio@capulet.lit/230193"); + di.setPacketID("disco1"); + di.setTo("juliet@capulet.lit/chamber"); + di.setType(IQ.Type.RESULT); + + Collection identities = new LinkedList(); + DiscoverInfo.Identity i = new DiscoverInfo.Identity("client", "Psi 0.11", "pc"); + i.setLanguage("en"); + identities.add(i); + i = new DiscoverInfo.Identity("client", "Ψ 0.11", "pc"); + i.setLanguage("el"); + identities.add(i); + di.addIdentities(identities); + + di.addFeature("http://jabber.org/protocol/disco#items"); + di.addFeature(EntityCapsManager.NAMESPACE); + di.addFeature("http://jabber.org/protocol/muc"); + di.addFeature("http://jabber.org/protocol/disco#info"); + + DataForm df = new DataForm("result"); + + FormField ff = new FormField("os"); + ff.addValue("Mac"); + df.addField(ff); + + ff = new FormField("FORM_TYPE"); + ff.setType("hidden"); + ff.addValue("urn:xmpp:dataforms:softwareinfo"); + df.addField(ff); + + ff = new FormField("ip_version"); + ff.addValue("ipv4"); + ff.addValue("ipv6"); + df.addField(ff); + + ff = new FormField("os_version"); + ff.addValue("10.5.1"); + df.addField(ff); + + ff = new FormField("software"); + ff.addValue("Psi"); + df.addField(ff); + + ff = new FormField("software_version"); + ff.addValue("0.11"); + df.addField(ff); + + di.addExtension(df); + return di; + } + + private static DiscoverInfo createMalformedDiscoverInfo() { + DiscoverInfo di = new DiscoverInfo(); + di.setFrom("benvolio@capulet.lit/230193"); + di.setPacketID("disco1"); + di.setTo(")juliet@capulet.lit/chamber"); + di.setType(IQ.Type.RESULT); + + Collection identities = new LinkedList(); + DiscoverInfo.Identity i = new DiscoverInfo.Identity("client", "Psi 0.11", "pc"); + i.setLanguage("en"); + identities.add(i); + i = new DiscoverInfo.Identity("client", "Ψ 0.11", "pc"); + i.setLanguage("el"); + identities.add(i); + di.addIdentities(identities); + // Failure 1: Duplicate identities + i = new DiscoverInfo.Identity("client", "Ψ 0.11", "pc"); + i.setLanguage("el"); + identities.add(i); + di.addIdentities(identities); + + di.addFeature("http://jabber.org/protocol/disco#items"); + di.addFeature(EntityCapsManager.NAMESPACE); + di.addFeature("http://jabber.org/protocol/muc"); + di.addFeature("http://jabber.org/protocol/disco#info"); + // Failure 2: Duplicate features + di.addFeature("http://jabber.org/protocol/disco#info"); + + DataForm df = new DataForm("result"); + + FormField ff = new FormField("os"); + ff.addValue("Mac"); + df.addField(ff); + + ff = new FormField("FORM_TYPE"); + ff.setType("hidden"); + ff.addValue("urn:xmpp:dataforms:softwareinfo"); + df.addField(ff); + + ff = new FormField("ip_version"); + ff.addValue("ipv4"); + ff.addValue("ipv6"); + df.addField(ff); + + ff = new FormField("os_version"); + ff.addValue("10.5.1"); + df.addField(ff); + + ff = new FormField("software"); + ff.addValue("Psi"); + df.addField(ff); + + ff = new FormField("software_version"); + ff.addValue("0.11"); + df.addField(ff); + + di.addExtension(df); + + // Failure 3: Another service discovery information form with the same + // FORM_TYPE + df = new DataForm("result"); + + ff = new FormField("FORM_TYPE"); + ff.setType("hidden"); + ff.addValue("urn:xmpp:dataforms:softwareinfo"); + df.addField(ff); + + ff = new FormField("software"); + ff.addValue("smack"); + df.addField(ff); + + di.addExtension(df); + + return di; + } + + public static File createTempDirectory() throws IOException { + String tmpdir = System.getProperty("java.io.tmpdir"); + File tmp; + tmp = File.createTempFile(tmpdir, "entityCaps"); + tmp.delete(); + tmp.mkdir(); + return tmp; + } + +} diff --git a/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java b/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java index 8f10e8e28..4b47aa2ff 100644 --- a/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java +++ b/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java @@ -50,8 +50,7 @@ public class ConfigureFormTest ThreadedDummyConnection con = new ThreadedDummyConnection(); PubSubManager mgr = new PubSubManager(con); DiscoverInfo info = new DiscoverInfo(); - Identity ident = new Identity("pubsub", null); - ident.setType("leaf"); + Identity ident = new Identity("pubsub", null, "leaf"); info.addIdentity(ident); con.addIQReply(info); @@ -78,8 +77,7 @@ public class ConfigureFormTest ThreadedDummyConnection con = new ThreadedDummyConnection(); PubSubManager mgr = new PubSubManager(con); DiscoverInfo info = new DiscoverInfo(); - Identity ident = new Identity("pubsub", null); - ident.setType("leaf"); + Identity ident = new Identity("pubsub", null, "leaf"); info.addIdentity(ident); con.addIQReply(info); diff --git a/test/org/jivesoftware/smack/ReconnectionTest.java b/test/org/jivesoftware/smack/ReconnectionTest.java index 8322ccef2..f4cbd0c32 100644 --- a/test/org/jivesoftware/smack/ReconnectionTest.java +++ b/test/org/jivesoftware/smack/ReconnectionTest.java @@ -40,7 +40,7 @@ public class ReconnectionTest extends SmackTestCase { public void testAutomaticReconnection() throws Exception { XMPPConnection connection = getConnection(0); - XMPPConnectionTestListener listener = new XMPPConnectionTestListener(); + XMPPConnectionTestListener listener = new XMPPConnectionTestListener(); connection.addConnectionListener(listener); // Simulates an error in the connection diff --git a/test/org/jivesoftware/smack/test/SmackTestCase.java b/test/org/jivesoftware/smack/test/SmackTestCase.java index 044a60cf4..fa6bb2d20 100644 --- a/test/org/jivesoftware/smack/test/SmackTestCase.java +++ b/test/org/jivesoftware/smack/test/SmackTestCase.java @@ -34,6 +34,7 @@ import junit.framework.TestCase; import org.jivesoftware.smack.ConnectionConfiguration; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.util.ConnectionUtils; import org.xmlpull.mxp1.MXParser; import org.xmlpull.v1.XmlPullParser; @@ -504,6 +505,15 @@ public abstract class SmackTestCase extends TestCase { return "config/" + fullClassName.substring(firstChar) + ".xml"; } + /** + * Subscribes all connections with each other: They all become friends + * + * @throws XMPPException + */ + protected void letsAllBeFriends() throws XMPPException { + ConnectionUtils.letsAllBeFriends(connections); + } + /** * Compares two contents of two byte arrays to make sure that they are equal * diff --git a/test/org/jivesoftware/smack/util/ConnectionUtils.java b/test/org/jivesoftware/smack/util/ConnectionUtils.java new file mode 100644 index 000000000..6d9f49106 --- /dev/null +++ b/test/org/jivesoftware/smack/util/ConnectionUtils.java @@ -0,0 +1,27 @@ +package org.jivesoftware.smack.util; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.Roster; +import org.jivesoftware.smack.XMPPException; + +public class ConnectionUtils { + + public static void becomeFriends(Connection con0, Connection con1) throws XMPPException { + Roster r0 = con0.getRoster(); + Roster r1 = con1.getRoster(); + r0.setSubscriptionMode(Roster.SubscriptionMode.accept_all); + r1.setSubscriptionMode(Roster.SubscriptionMode.accept_all); + r0.createEntry(con1.getUser(), "u2", null); + r1.createEntry(con0.getUser(), "u1", null); + } + + public static void letsAllBeFriends(Connection[] connections) throws XMPPException { + for (Connection c1 : connections) { + for (Connection c2 : connections) { + if (c1 == c2) + continue; + becomeFriends(c1, c2); + } + } + } +} diff --git a/test/org/jivesoftware/smackx/ServiceDiscoveryManagerTest.java b/test/org/jivesoftware/smackx/ServiceDiscoveryManagerTest.java index 9a902ab31..9ba422e3e 100644 --- a/test/org/jivesoftware/smackx/ServiceDiscoveryManagerTest.java +++ b/test/org/jivesoftware/smackx/ServiceDiscoveryManagerTest.java @@ -48,7 +48,7 @@ public class ServiceDiscoveryManagerTest extends SmackTestCase { .getInstanceFor(getConnection(0)); try { // Discover the information of another Smack client - DiscoverInfo info = discoManager.discoverInfo(getFullJID(1)); + DiscoverInfo info = discoManager.discoverInfo(getFullJID(1)); // Check the identity of the Smack client Iterator identities = info.getIdentities(); assertTrue("No identities were found", identities.hasNext()); diff --git a/test/org/jivesoftware/smackx/entitycaps/EntityCapsTest.java b/test/org/jivesoftware/smackx/entitycaps/EntityCapsTest.java new file mode 100644 index 000000000..49a81f3e4 --- /dev/null +++ b/test/org/jivesoftware/smackx/entitycaps/EntityCapsTest.java @@ -0,0 +1,147 @@ +package org.jivesoftware.smackx.entitycaps; + +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.filter.IQTypeFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.test.SmackTestCase; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; + +public class EntityCapsTest extends SmackTestCase { + + private static final String DISCOVER_TEST_FEATURE = "entityCapsTest"; + + XMPPConnection con0; + XMPPConnection con1; + EntityCapsManager ecm0; + EntityCapsManager ecm1; + ServiceDiscoveryManager sdm0; + ServiceDiscoveryManager sdm1; + + private boolean discoInfoSend = false; + + public EntityCapsTest(String arg0) { + super(arg0); + } + + @Override + protected int getMaxConnections() { + return 2; + } + + protected void setUp() throws Exception { + super.setUp(); + SmackConfiguration.setAutoEnableEntityCaps(true); + SmackConfiguration.setPacketReplyTimeout(1000 * 60 * 5); + con0 = getConnection(0); + con1 = getConnection(1); + ecm0 = EntityCapsManager.getInstanceFor(getConnection(0)); + ecm1 = EntityCapsManager.getInstanceFor(getConnection(1)); + sdm0 = ServiceDiscoveryManager.getInstanceFor(con0); + sdm1 = ServiceDiscoveryManager.getInstanceFor(con1); + letsAllBeFriends(); + } + + public void testLocalEntityCaps() throws InterruptedException { + DiscoverInfo info = EntityCapsManager.getDiscoveryInfoByNodeVer(ecm1.getLocalNodeVer()); + assertFalse(info.containsFeature(DISCOVER_TEST_FEATURE)); + + dropWholeEntityCapsCache(); + + // This should cause a new presence stanza from con1 with and updated + // 'ver' String + sdm1.addFeature(DISCOVER_TEST_FEATURE); + + // Give the server some time to handle the stanza and send it to con0 + Thread.sleep(2000); + + // The presence stanza should get received by con0 and the data should + // be recorded in the map + // Note that while both connections use the same static Entity Caps + // cache, + // it's assured that *not* con1 added the data to the Entity Caps cache. + // Every time the entities features + // and identities change only a new caps 'ver' is calculated and send + // with the presence stanza + // The other connection has to receive this stanza and record the + // information in order for this test to succeed. + info = EntityCapsManager.getDiscoveryInfoByNodeVer(ecm1.getLocalNodeVer()); + assertNotNull(info); + assertTrue(info.containsFeature(DISCOVER_TEST_FEATURE)); + } + + /** + * Test if entity caps actually prevent a disco info request and reply + * + * @throws XMPPException + * + */ + public void testPreventDiscoInfo() throws XMPPException { + con0.addPacketSendingListener(new PacketListener() { + + @Override + public void processPacket(Packet packet) { + discoInfoSend = true; + } + + }, new AndFilter(new PacketTypeFilter(DiscoverInfo.class), new IQTypeFilter(IQ.Type.GET))); + + // add a bogus feature so that con1 ver won't match con0's + sdm1.addFeature(DISCOVER_TEST_FEATURE); + + dropCapsCache(); + // discover that + DiscoverInfo info = sdm0.discoverInfo(con1.getUser()); + // that discovery should cause a disco#info + assertTrue(discoInfoSend); + assertTrue(info.containsFeature(DISCOVER_TEST_FEATURE)); + discoInfoSend = false; + + // discover that + info = sdm0.discoverInfo(con1.getUser()); + // that discovery shouldn't cause a disco#info + assertFalse(discoInfoSend); + assertTrue(info.containsFeature(DISCOVER_TEST_FEATURE)); + } + + public void testCapsChanged() { + String nodeVerBefore = EntityCapsManager.getNodeVersionByJid(con1.getUser()); + sdm1.addFeature(DISCOVER_TEST_FEATURE); + String nodeVerAfter = EntityCapsManager.getNodeVersionByJid(con1.getUser()); + + assertFalse(nodeVerBefore.equals(nodeVerAfter)); + } + + public void testEntityCaps() throws XMPPException, InterruptedException { + dropWholeEntityCapsCache(); + sdm1.addFeature(DISCOVER_TEST_FEATURE); + + Thread.sleep(3000); + + DiscoverInfo info = sdm0.discoverInfo(con1.getUser()); + assertTrue(info.containsFeature(DISCOVER_TEST_FEATURE)); + + String u1ver = EntityCapsManager.getNodeVersionByJid(con1.getUser()); + assertNotNull(u1ver); + + DiscoverInfo entityInfo = EntityCapsManager.caps.get(u1ver); + assertNotNull(entityInfo); + + assertEquals(info.toXML(), entityInfo.toXML()); + } + + private static void dropWholeEntityCapsCache() { + EntityCapsManager.caps.clear(); + EntityCapsManager.jidCaps.clear(); + } + + private static void dropCapsCache() { + EntityCapsManager.caps.clear(); + } +} From 2eb13f48d2a72af9f0e88c5b2d8a9881a655fa1b Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 18 Mar 2013 08:53:11 +0000 Subject: [PATCH 06/41] SMACK-225 Fixed DNS SRV handling, as per RFC 2782. Added support for multiple DNS SRV resolvers namely javax and org.xbill.dns (aka dnsjava). git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13561 b35dd754-fafc-0310-a699-88a17e54d16e --- build/eclipse/classpath | 1 + build/merge/org.xbill.dns_2.1.4.jar | Bin 0 -> 304384 bytes build/resources/META-INF/smack-config.xml | 9 +- .../smack/ConnectionConfiguration.java | 54 ++- .../jivesoftware/smack/XMPPConnection.java | 72 ++-- .../org/jivesoftware/smack/util/DNSUtil.java | 318 ++++++++---------- .../smack/util/dns/DNSJavaResolver.java | 72 ++++ .../smack/util/dns/DNSResolver.java | 24 ++ .../smack/util/dns/HostAddress.java | 93 +++++ .../smack/util/dns/JavaxResolver.java | 99 ++++++ .../smack/util/dns/SRVRecord.java | 77 +++++ .../jivesoftware/smack/util/DNSUtilTest.java | 147 ++++++++ 12 files changed, 756 insertions(+), 210 deletions(-) create mode 100644 build/merge/org.xbill.dns_2.1.4.jar create mode 100644 source/org/jivesoftware/smack/util/dns/DNSJavaResolver.java create mode 100644 source/org/jivesoftware/smack/util/dns/DNSResolver.java create mode 100644 source/org/jivesoftware/smack/util/dns/HostAddress.java create mode 100644 source/org/jivesoftware/smack/util/dns/JavaxResolver.java create mode 100644 source/org/jivesoftware/smack/util/dns/SRVRecord.java create mode 100644 test-unit/org/jivesoftware/smack/util/DNSUtilTest.java diff --git a/build/eclipse/classpath b/build/eclipse/classpath index ddf552712..67dd4e3b5 100644 --- a/build/eclipse/classpath +++ b/build/eclipse/classpath @@ -28,5 +28,6 @@ + diff --git a/build/merge/org.xbill.dns_2.1.4.jar b/build/merge/org.xbill.dns_2.1.4.jar new file mode 100644 index 0000000000000000000000000000000000000000..ca8c76c81534bd4ad1bec77c47d43973108f7105 GIT binary patch literal 304384 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�!6Fc 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

*Xx^t?dJG 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 Ii2pGE59p;z!~g&Q literal 0 HcmV?d00001 diff --git a/build/resources/META-INF/smack-config.xml b/build/resources/META-INF/smack-config.xml index 6c4b891e2..1c763e0c2 100644 --- a/build/resources/META-INF/smack-config.xml +++ b/build/resources/META-INF/smack-config.xml @@ -5,15 +5,16 @@ org.jivesoftware.smackx.ServiceDiscoveryManager - org.jivesoftware.smack.PrivacyListManager + org.jivesoftware.smack.PrivacyListManager org.jivesoftware.smackx.XHTMLManager org.jivesoftware.smackx.muc.MultiUserChat org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager - org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager + org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager org.jivesoftware.smackx.filetransfer.FileTransferManager org.jivesoftware.smackx.LastActivityManager - org.jivesoftware.smack.ReconnectionManager - org.jivesoftware.smackx.commands.AdHocCommandManager + org.jivesoftware.smack.ReconnectionManager + org.jivesoftware.smackx.commands.AdHocCommandManager + org.jivesoftware.smack.util.dns.JavaxResolver diff --git a/source/org/jivesoftware/smack/ConnectionConfiguration.java b/source/org/jivesoftware/smack/ConnectionConfiguration.java index 24389ac6f..9eb7bb903 100644 --- a/source/org/jivesoftware/smack/ConnectionConfiguration.java +++ b/source/org/jivesoftware/smack/ConnectionConfiguration.java @@ -22,11 +22,15 @@ 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 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 @@ -48,6 +52,7 @@ public class ConnectionConfiguration implements Cloneable { private String host; private int port; + protected List hostAddresses; private String truststorePath; private String truststoreType; @@ -98,12 +103,11 @@ public class ConnectionConfiguration implements Cloneable { */ public ConnectionConfiguration(String serviceName) { // Perform DNS lookup to get host and port to use - DNSUtil.HostAddress address = DNSUtil.resolveXMPPDomain(serviceName); - init(address.getHost(), address.getPort(), serviceName, - ProxyInfo.forDefaultProxy()); + hostAddresses = DNSUtil.resolveXMPPDomain(serviceName); + init(serviceName, ProxyInfo.forDefaultProxy()); } - - /** + + /** * 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 @@ -114,8 +118,8 @@ public class ConnectionConfiguration implements Cloneable { */ public ConnectionConfiguration(String serviceName,ProxyInfo proxy) { // Perform DNS lookup to get host and port to use - DNSUtil.HostAddress address = DNSUtil.resolveXMPPDomain(serviceName); - init(address.getHost(), address.getPort(), serviceName, proxy); + hostAddresses = DNSUtil.resolveXMPPDomain(serviceName); + init(serviceName, proxy); } /** @@ -133,7 +137,8 @@ public class ConnectionConfiguration implements Cloneable { * @param serviceName the name of the service provided by an XMPP server. */ public ConnectionConfiguration(String host, int port, String serviceName) { - init(host, port, serviceName, ProxyInfo.forDefaultProxy()); + initHostAddresses(host, port); + init(serviceName, ProxyInfo.forDefaultProxy()); } /** @@ -152,7 +157,8 @@ public class ConnectionConfiguration implements Cloneable { * @param proxy the proxy through which XMPP is to be connected */ public ConnectionConfiguration(String host, int port, String serviceName, ProxyInfo proxy) { - init(host, port, serviceName, proxy); + initHostAddresses(host, port); + init(serviceName, proxy); } /** @@ -163,7 +169,8 @@ public class ConnectionConfiguration implements Cloneable { * @param port the port where the XMPP is listening. */ public ConnectionConfiguration(String host, int port) { - init(host, port, host, ProxyInfo.forDefaultProxy()); + initHostAddresses(host, port); + init(host, ProxyInfo.forDefaultProxy()); } /** @@ -175,12 +182,11 @@ public class ConnectionConfiguration implements Cloneable { * @param proxy the proxy through which XMPP is to be connected */ public ConnectionConfiguration(String host, int port, ProxyInfo proxy) { - init(host, port, host, proxy); + initHostAddresses(host, port); + init(host, proxy); } - private void init(String host, int port, String serviceName, ProxyInfo proxy) { - this.host = host; - this.port = port; + protected void init(String serviceName, ProxyInfo proxy) { this.serviceName = serviceName; this.proxy = proxy; @@ -243,6 +249,11 @@ public class ConnectionConfiguration implements Cloneable { 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}. @@ -674,6 +685,10 @@ public class ConnectionConfiguration implements Cloneable { return this.socketFactory; } + public List getHostAddresses() { + return Collections.unmodifiableList(hostAddresses); + } + /** * An enumeration for TLS security modes that are available when making a connection * to the XMPP server. @@ -742,4 +757,15 @@ public class ConnectionConfiguration implements Cloneable { this.password = password; this.resource = resource; } + + private void initHostAddresses(String host, int port) { + hostAddresses = new ArrayList(1); + HostAddress hostAddress; + try { + hostAddress = new HostAddress(host, port); + } catch (Exception e) { + throw new IllegalStateException(e); + } + hostAddresses.add(hostAddress); + } } diff --git a/source/org/jivesoftware/smack/XMPPConnection.java b/source/org/jivesoftware/smack/XMPPConnection.java index 4024bd340..af7b66750 100644 --- a/source/org/jivesoftware/smack/XMPPConnection.java +++ b/source/org/jivesoftware/smack/XMPPConnection.java @@ -26,6 +26,7 @@ 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; @@ -51,6 +52,9 @@ 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 @@ -546,27 +550,57 @@ public class XMPPConnection extends Connection { } private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException { - String host = config.getHost(); - int port = config.getPort(); - try { - if (config.getSocketFactory() == null) { - this.socket = new Socket(host, port); + XMPPException exception = null; + Iterator it = config.getHostAddresses().iterator(); + List failedAddresses = new LinkedList(); + 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; } - else { - this.socket = config.getSocketFactory().createSocket(host, port); + 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); } - } - catch (UnknownHostException uhe) { - String errorMessage = "Could not connect to " + host + ":" + port + "."; - throw new XMPPException(errorMessage, new XMPPError( - XMPPError.Condition.remote_server_timeout, errorMessage), - uhe); - } - catch (IOException ioe) { - String errorMessage = "XMPPError connecting to " + host + ":" - + port + "."; - throw new XMPPException(errorMessage, new XMPPError( - XMPPError.Condition.remote_server_error, errorMessage), ioe); } socketClosed = false; initConnection(); diff --git a/source/org/jivesoftware/smack/util/DNSUtil.java b/source/org/jivesoftware/smack/util/DNSUtil.java index ab3e2e44d..628d8e8f3 100644 --- a/source/org/jivesoftware/smack/util/DNSUtil.java +++ b/source/org/jivesoftware/smack/util/DNSUtil.java @@ -19,18 +19,20 @@ package org.jivesoftware.smack.util; -import java.util.Hashtable; +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 javax.naming.NamingEnumeration; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.DirContext; -import javax.naming.directory.InitialDirContext; - +import org.jivesoftware.smack.util.dns.DNSResolver; +import org.jivesoftware.smack.util.dns.HostAddress; +import org.jivesoftware.smack.util.dns.SRVRecord; /** - * Utilty class to perform DNS lookups for XMPP services. + * Utility class to perform DNS lookups for XMPP services. * * @author Matt Tucker */ @@ -40,23 +42,30 @@ public class DNSUtil { * Create a cache to hold the 100 most recently accessed DNS lookups for a period of * 10 minutes. */ - private static Map cache = new Cache(100, 1000*60*10); + private static Map> cache = new Cache>(100, 1000*60*10); - private static DirContext context; + private static DNSResolver dnsResolver = null; - static { - try { - Hashtable env = new Hashtable(); - env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); - context = new InitialDirContext(env); - } - catch (Exception e) { - // Ignore. - } + /** + * Set the DNS resolver that should be used to perform DNS lookups. + * + * @param resolver + */ + public static void setDNSResolver(DNSResolver resolver) { + dnsResolver = resolver; } /** - * Returns the host name and port that the specified XMPP server can be + * 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 @@ -67,81 +76,17 @@ public class DNSUtil { * of 5222.

* * As an example, a lookup for "example.com" may return "im.example.com:5269". - * - * Note on SRV record selection. - * We now check priority and weight, but we still don't do this correctly. - * The missing behavior is this: if we fail to reach a host based on its SRV - * record then we need to select another host from the other SRV records. - * In Smack 3.1.1 we're not going to be able to do the major system redesign to - * correct this. * * @param domain the domain. - * @return a HostAddress, which encompasses the hostname and port that the XMPP - * server can be reached at for the specified domain. + * @return List of HostAddress, which encompasses the hostname and port that the + * XMPP server can be reached at for the specified domain. */ - public static HostAddress resolveXMPPDomain(String domain) { - if (context == null) { - return new HostAddress(domain, 5222); - } - String key = "c" + domain; - // Return item from cache if it exists. - if (cache.containsKey(key)) { - HostAddress address = (HostAddress)cache.get(key); - if (address != null) { - return address; - } - } - String bestHost = domain; - int bestPort = 5222; - int bestPriority = 0; - int bestWeight = 0; - try { - Attributes dnsLookup = context.getAttributes("_xmpp-client._tcp." + domain, new String[]{"SRV"}); - Attribute srvAttribute = dnsLookup.get("SRV"); - NamingEnumeration srvRecords = (NamingEnumeration) srvAttribute.getAll(); - while(srvRecords.hasMore()) { - String srvRecord = srvRecords.next(); - String [] srvRecordEntries = srvRecord.split(" "); - int priority = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 4]); - int port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); - int weight = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 3]); - String host = srvRecordEntries[srvRecordEntries.length-1]; - - // Randomize the weight. - weight *= Math.random() * weight; - - if ((bestPriority == 0) || (priority < bestPriority)) { - // Choose a server with the lowest priority. - bestPriority = priority; - bestWeight = weight; - bestHost = host; - bestPort = port; - } else if (priority == bestPriority) { - // When we have like priorities then randomly choose a server based on its weight - // The weights were randomized above. - if (weight > bestWeight) { - bestWeight = weight; - bestHost = host; - bestPort = port; - } - } - } - } - catch (Exception e) { - // Ignore. - } - // Host entries in DNS should end with a ".". - if (bestHost.endsWith(".")) { - bestHost = bestHost.substring(0, bestHost.length()-1); - } - HostAddress address = new HostAddress(bestHost, bestPort); - // Add item to cache. - cache.put(key, address); - return address; + public static List resolveXMPPDomain(String domain) { + return resolveDomain(domain, 'c'); } /** - * Returns the host name and port that the specified XMPP server can be + * 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 @@ -154,104 +99,131 @@ public class DNSUtil { * As an example, a lookup for "example.com" may return "im.example.com:5269". * * @param domain the domain. - * @return a HostAddress, which encompasses the hostname and port that the XMPP - * server can be reached at for the specified domain. + * @return List of HostAddress, which encompasses the hostname and port that the + * XMPP server can be reached at for the specified domain. */ - public static HostAddress resolveXMPPServerDomain(String domain) { - if (context == null) { - return new HostAddress(domain, 5269); - } - String key = "s" + domain; + public static List resolveXMPPServerDomain(String domain) { + return resolveDomain(domain, 's'); + } + + private static List 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)) { - HostAddress address = (HostAddress)cache.get(key); - if (address != null) { - return address; + List addresses = cache.get(key); + if (addresses != null) { + return addresses; } } - String host = domain; - int port = 5269; - try { - Attributes dnsLookup = - context.getAttributes("_xmpp-server._tcp." + domain, new String[]{"SRV"}); - String srvRecord = (String)dnsLookup.get("SRV").get(); - String [] srvRecordEntries = srvRecord.split(" "); - port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); - host = srvRecordEntries[srvRecordEntries.length-1]; + + if (dnsResolver == null) + throw new IllegalStateException("No DNS resolver active."); + + List addresses = new ArrayList(); + + // 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; } - catch (Exception e) { - // Attempt lookup with older "jabber" name. - try { - Attributes dnsLookup = - context.getAttributes("_jabber._tcp." + domain, new String[]{"SRV"}); - String srvRecord = (String)dnsLookup.get("SRV").get(); - String [] srvRecordEntries = srvRecord.split(" "); - port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); - host = srvRecordEntries[srvRecordEntries.length-1]; - } - catch (Exception e2) { - // Ignore. - } - } - // Host entries in DNS should end with a ".". - if (host.endsWith(".")) { - host = host.substring(0, host.length()-1); - } - HostAddress address = new HostAddress(host, port); + List srvRecords = dnsResolver.lookupSRVRecords(srvDomain); + List 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, address); - return address; + cache.put(key, addresses); + + return addresses; } /** - * Encapsulates a hostname and port. + * 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 */ - public static class HostAddress { + protected static List sortSRVRecords(List 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; - private String host; - private int port; + // sorting the records improves the performance of the bisection later + Collections.sort(records); - private HostAddress(String host, int port) { - this.host = host; - this.port = port; - } - - /** - * Returns the hostname. - * - * @return the hostname. - */ - public String getHost() { - return host; - } - - /** - * Returns the port. - * - * @return the port. - */ - public int getPort() { - return port; - } - - public String toString() { - return host + ":" + port; - } - - public boolean equals(Object o) { - if (this == o) { - return true; + // create the priority buckets + SortedMap> buckets = new TreeMap>(); + for (SRVRecord r : records) { + Integer priority = r.getPriority(); + List bucket = buckets.get(priority); + // create the list of SRVRecords if it doesn't exist + if (bucket == null) { + bucket = new LinkedList(); + buckets.put(priority, bucket); } - if (!(o instanceof HostAddress)) { - return false; - } - - final HostAddress address = (HostAddress) o; - - if (!host.equals(address.host)) { - return false; - } - return port == address.port; + bucket.add(r); } + + List res = new ArrayList(records.size()); + + for (Integer priority : buckets.keySet()) { + List 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/source/org/jivesoftware/smack/util/dns/DNSJavaResolver.java b/source/org/jivesoftware/smack/util/dns/DNSJavaResolver.java new file mode 100644 index 000000000..91db73b1a --- /dev/null +++ b/source/org/jivesoftware/smack/util/dns/DNSJavaResolver.java @@ -0,0 +1,72 @@ +/** + * 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; + +public class DNSJavaResolver extends DNSResolver { + + private static DNSJavaResolver instance; + + private DNSJavaResolver() { + + } + + public static DNSResolver getInstance() { + if (instance == null) { + instance = new DNSJavaResolver(); + } + return instance; + } + + @Override + public List lookupSRVRecords(String name) { + List res = new ArrayList(); + + 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/source/org/jivesoftware/smack/util/dns/DNSResolver.java b/source/org/jivesoftware/smack/util/dns/DNSResolver.java new file mode 100644 index 000000000..2c5dd296c --- /dev/null +++ b/source/org/jivesoftware/smack/util/dns/DNSResolver.java @@ -0,0 +1,24 @@ +/** + * 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; + +public abstract class DNSResolver { + + public abstract List lookupSRVRecords(String name); + +} diff --git a/source/org/jivesoftware/smack/util/dns/HostAddress.java b/source/org/jivesoftware/smack/util/dns/HostAddress.java new file mode 100644 index 000000000..978a6de69 --- /dev/null +++ b/source/org/jivesoftware/smack/util/dns/HostAddress.java @@ -0,0 +1,93 @@ +/** + * 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 + * @throws IllegalArgumentException + */ + public HostAddress(String fqdn) throws IllegalArgumentException { + 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; + } + + public HostAddress(String fqdn, int port) throws IllegalArgumentException { + 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; + } + + public String toString() { + return fqdn + ":" + port; + } + + 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; + } + + public String getErrorMessage() { + String error; + if (exception == null) { + error = "No error logged"; + } + else { + error = exception.getMessage(); + } + return toString() + " Exception: " + error; + } +} diff --git a/source/org/jivesoftware/smack/util/dns/JavaxResolver.java b/source/org/jivesoftware/smack/util/dns/JavaxResolver.java new file mode 100644 index 000000000..4ea361fdf --- /dev/null +++ b/source/org/jivesoftware/smack/util/dns/JavaxResolver.java @@ -0,0 +1,99 @@ +/** + * 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.Hashtable; +import java.util.List; + +import javax.naming.NamingEnumeration; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; + +import org.jivesoftware.smack.util.DNSUtil; + +/** + * A DNS resolver (mostly for SRV records), which makes use of the API provided in the javax.* namepsace. + * + * @author Florian Schmaus + * + */ +public class JavaxResolver extends DNSResolver { + + private static JavaxResolver instance; + private static DirContext dirContext; + + static { + try { + Hashtable env = new Hashtable(); + env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + dirContext = new InitialDirContext(env); + } catch (Exception e) { + // Ignore. + } + + // Try to set this DNS resolver as primary one + DNSUtil.setDNSResolver(maybeGetInstance()); + } + + private JavaxResolver() { + + } + + public static DNSResolver maybeGetInstance() { + if (instance == null && isSupported()) { + instance = new JavaxResolver(); + } + return instance; + } + + public static boolean isSupported() { + return dirContext != null; + } + + @Override + public List lookupSRVRecords(String name) { + List res = new ArrayList(); + + try { + Attributes dnsLookup = dirContext.getAttributes(name, new String[]{"SRV"}); + Attribute srvAttribute = dnsLookup.get("SRV"); + @SuppressWarnings("unchecked") + NamingEnumeration srvRecords = (NamingEnumeration) srvAttribute.getAll(); + while (srvRecords.hasMore()) { + String srvRecordString = srvRecords.next(); + String[] srvRecordEntries = srvRecordString.split(" "); + int priority = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 4]); + int port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 2]); + int weight = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 3]); + String host = srvRecordEntries[srvRecordEntries.length - 1]; + + SRVRecord srvRecord; + try { + srvRecord = new SRVRecord(host, port, priority, weight); + } catch (Exception e) { + continue; + } + res.add(srvRecord); + } + } catch (Exception e) { + + } + return res; + } +} diff --git a/source/org/jivesoftware/smack/util/dns/SRVRecord.java b/source/org/jivesoftware/smack/util/dns/SRVRecord.java new file mode 100644 index 000000000..87c6e54fc --- /dev/null +++ b/source/org/jivesoftware/smack/util/dns/SRVRecord.java @@ -0,0 +1,77 @@ +/** + * 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 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; + } + + 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; + } + + public String toString() { + return super.toString() + " prio:" + priority + ":w:" + weight; + } +} diff --git a/test-unit/org/jivesoftware/smack/util/DNSUtilTest.java b/test-unit/org/jivesoftware/smack/util/DNSUtilTest.java new file mode 100644 index 000000000..d107f8f0d --- /dev/null +++ b/test-unit/org/jivesoftware/smack/util/DNSUtilTest.java @@ -0,0 +1,147 @@ +package org.jivesoftware.smack.util; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; + +import java.util.ArrayList; +import java.util.List; + + +import org.jivesoftware.smack.util.dns.DNSJavaResolver; +import org.jivesoftware.smack.util.dns.DNSResolver; +import org.jivesoftware.smack.util.dns.HostAddress; +import org.jivesoftware.smack.util.dns.JavaxResolver; +import org.jivesoftware.smack.util.dns.SRVRecord; +import org.junit.Test; + +public class DNSUtilTest { + private static final String igniterealtimeDomain = "igniterealtime.org"; + private static final String igniterealtimeXMPPServer = "xmpp." + igniterealtimeDomain; + private static final int igniterealtimeClientPort = 5222; + private static final int igniterealtimeServerPort = 5269; + + @Test + public void xmppClientDomainJavaXTest() { + DNSResolver resolver = JavaxResolver.maybeGetInstance(); + assertNotNull(resolver); + DNSUtil.setDNSResolver(resolver); + xmppClientDomainTest(); + } + + @Test + public void xmppServerDomainJavaXTest() { + DNSResolver resolver = JavaxResolver.maybeGetInstance(); + assertNotNull(resolver); + DNSUtil.setDNSResolver(resolver); + xmppServerDomainTest(); + } + + @Test + public void xmppClientDomainDNSJavaTest() { + DNSResolver resolver = DNSJavaResolver.getInstance(); + assertNotNull(resolver); + DNSUtil.setDNSResolver(resolver); + xmppClientDomainTest(); + } + + @Test + public void xmppServerDomainDNSJavaTest() { + DNSResolver resolver = DNSJavaResolver.getInstance(); + assertNotNull(resolver); + DNSUtil.setDNSResolver(resolver); + xmppServerDomainTest(); + } + + @Test + public void sortSRVlowestPrioFirstTest() { + List sortedRecords = DNSUtil.sortSRVRecords(createSRVRecords()); + assertTrue(sortedRecords.get(0).getFQDN().equals("0.20.foo.bar")); + } + + @Test + public void sortSRVdistributeOverWeights() { + int weight50 = 0; + int weight20one = 0; + int weight20two = 0; + int weight10 = 0; + for (int i = 0; i < 1000; i++) { + List sortedRecords = DNSUtil.sortSRVRecords(createSRVRecords()); + String host = sortedRecords.get(1).getFQDN(); + if (host.equals("5.20.one.foo.bar")) { + weight20one++; + } else if (host.equals("5.20.two.foo.bar")) { + weight20two++; + } else if (host.equals("5.10.foo.bar")) { + weight10++; + } else if (host.equals("5.50.foo.bar")) { + weight50++; + } else { + fail("Wrong host after SRVRecord sorting"); + } + } + assertTrue(weight50 > 400 && weight50 < 600); + assertTrue(weight20one > 100 && weight20one < 300); + assertTrue(weight20two > 100 && weight20two < 300); + assertTrue(weight10 > 0&& weight10 < 200); + } + + @Test + public void sortSRVdistributeZeroWeights() { + int weightZeroOne = 0; + int weightZeroTwo = 0; + for (int i = 0; i < 1000; i++) { + List sortedRecords = DNSUtil.sortSRVRecords(createSRVRecords()); + // Remove the first 5 records with a lower priority + for (int j = 0; j < 5; j++) { + sortedRecords.remove(0); + } + String host = sortedRecords.remove(0).getFQDN(); + if (host.equals("10.0.one.foo.bar")) { + weightZeroOne++; + } else if (host.endsWith("10.0.two.foo.bar")) { + weightZeroTwo++; + } else { + fail("Wrong host after SRVRecord sorting"); + } + } + assertTrue(weightZeroOne > 400 && weightZeroOne < 600); + assertTrue(weightZeroTwo > 400 && weightZeroTwo < 600); + } + + private void xmppClientDomainTest() { + List hostAddresses = DNSUtil.resolveXMPPDomain(igniterealtimeDomain); + HostAddress ha = hostAddresses.get(0); + assertEquals(ha.getFQDN(), igniterealtimeXMPPServer); + assertEquals(ha.getPort(), igniterealtimeClientPort); + } + + private void xmppServerDomainTest() { + List hostAddresses = DNSUtil.resolveXMPPServerDomain(igniterealtimeDomain); + HostAddress ha = hostAddresses.get(0); + assertEquals(ha.getFQDN(), igniterealtimeXMPPServer); + assertEquals(ha.getPort(), igniterealtimeServerPort); + } + + private static List createSRVRecords() { + List records = new ArrayList(); + // We create one record with priority 0 that should also be tried first + // Then 4 records with priority 5 and different weights (50, 20, 20, 10) + // Then 2 records with priority 10 and weight 0 which should be treaded equal + // These records are added in a 'random' way to the list + try { + records.add(new SRVRecord("5.20.one.foo.bar", 42, 5, 20)); // Priority 5, Weight 20 + records.add(new SRVRecord("10.0.one.foo.bar", 42, 10, 0)); // Priority 10, Weight 0 + records.add(new SRVRecord("5.10.foo.bar", 42, 5, 10)); // Priority 5, Weight 10 + records.add(new SRVRecord("10.0.two.foo.bar", 42, 10, 0)); // Priority 10, Weight 0 + records.add(new SRVRecord("5.20.two.foo.bar", 42, 5, 20)); // Priority 5, Weight 20 + records.add(new SRVRecord("0.20.foo.bar", 42, 0, 20)); // Priority 0, Weight 20 + records.add(new SRVRecord("5.50.foo.bar", 42, 5, 50)); // Priority 5, Weight 50 + } catch (IllegalArgumentException e) { + // Ignore + } + assertTrue(records.size() > 0); + return records; + } +} From 6dcf8e0123c3599f5ac43ab1c5014c3735c857c2 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 18 Mar 2013 19:58:14 +0000 Subject: [PATCH 07/41] SMACK-419 PacketWriter: Only flush if queue is empty git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13567 b35dd754-fafc-0310-a699-88a17e54d16e --- source/org/jivesoftware/smack/PacketWriter.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/source/org/jivesoftware/smack/PacketWriter.java b/source/org/jivesoftware/smack/PacketWriter.java index 8213bbd35..155110cfb 100644 --- a/source/org/jivesoftware/smack/PacketWriter.java +++ b/source/org/jivesoftware/smack/PacketWriter.java @@ -193,9 +193,11 @@ class PacketWriter { if (packet != null) { synchronized (writer) { writer.write(packet.toXML()); - writer.flush(); - // Keep track of the last time a stanza was sent to the server - lastActive = System.currentTimeMillis(); + if (queue.isEmpty()) { + writer.flush(); + // Keep track of the last time a stanza was sent to the server + lastActive = System.currentTimeMillis(); + } } } } From a55b54f20b80a7a9338f381faaeeef272da22884 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 18 Mar 2013 19:58:48 +0000 Subject: [PATCH 08/41] SMACK-417 If both PacketReader and PacketWriter fail at the same time, connectionClosedonError() is called two times Refactored notifyConnectionError() and notifyReconnection() from PacketReader to XMPPConnection. Made PacketReader.done and PacketWriter.done volatile. Prevent duplicate connectionClosedonError() calls by making the method synchronzied and protected them with an enter guard: if (packetReader.done && packetWriter.done) return; git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13568 b35dd754-fafc-0310-a699-88a17e54d16e --- .../org/jivesoftware/smack/PacketReader.java | 46 +------------ .../org/jivesoftware/smack/PacketWriter.java | 4 +- .../jivesoftware/smack/XMPPConnection.java | 68 ++++++++++++++++--- .../jivesoftware/smack/ReconnectionTest.java | 6 +- .../jivesoftware/smack/RosterSmackTest.java | 2 +- 5 files changed, 65 insertions(+), 61 deletions(-) diff --git a/source/org/jivesoftware/smack/PacketReader.java b/source/org/jivesoftware/smack/PacketReader.java index 590dfd951..af1c7682f 100644 --- a/source/org/jivesoftware/smack/PacketReader.java +++ b/source/org/jivesoftware/smack/PacketReader.java @@ -48,7 +48,7 @@ class PacketReader { private XMPPConnection connection; private XmlPullParser parser; - private boolean done; + volatile boolean done; private String connectionID = null; private Semaphore connectionSemaphore; @@ -155,48 +155,6 @@ class PacketReader { connection.collectors.clear(); } - /** - * Sends out a notification that there was an error with the connection - * and closes the connection. - * - * @param e the exception that causes the connection close event. - */ - void notifyConnectionError(Exception e) { - done = true; - // Closes the connection temporary. A reconnection is possible - connection.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 : connection.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 : connection.getConnectionListeners()) { - try { - listener.reconnectionSuccessful(); - } - catch (Exception e) { - // Catch and print any exception so we can recover - // from a faulty listener - e.printStackTrace(); - } - } - } - /** * 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 @@ -332,7 +290,7 @@ class PacketReader { if (!(done || connection.isSocketClosed())) { // Close the connection and notify connection listeners of the // error. - notifyConnectionError(e); + connection.notifyConnectionError(e); } } } diff --git a/source/org/jivesoftware/smack/PacketWriter.java b/source/org/jivesoftware/smack/PacketWriter.java index 155110cfb..f8b5c359f 100644 --- a/source/org/jivesoftware/smack/PacketWriter.java +++ b/source/org/jivesoftware/smack/PacketWriter.java @@ -44,7 +44,7 @@ class PacketWriter { private Writer writer; private XMPPConnection connection; private final BlockingQueue queue; - private boolean done; + volatile boolean done; /** * Timestamp when the last stanza was sent to the server. This information is used @@ -245,7 +245,7 @@ class PacketWriter { // packetReader could be set to null by an concurrent disconnect() call. // Therefore Prevent NPE exceptions by checking packetReader. if (connection.packetReader != null) { - connection.packetReader.notifyConnectionError(ioe); + connection.notifyConnectionError(ioe); } } } diff --git a/source/org/jivesoftware/smack/XMPPConnection.java b/source/org/jivesoftware/smack/XMPPConnection.java index af7b66750..6512aa981 100644 --- a/source/org/jivesoftware/smack/XMPPConnection.java +++ b/source/org/jivesoftware/smack/XMPPConnection.java @@ -473,6 +473,10 @@ public class XMPPConnection extends Connection { return; } + if (!isConnected()) { + return; + } + shutdown(unavailablePresence); if (roster != null) { @@ -483,9 +487,7 @@ public class XMPPConnection extends Connection { wasAuthenticated = false; packetWriter.cleanup(); - packetWriter = null; packetReader.cleanup(); - packetReader = null; } public void sendPacket(Packet packet) { @@ -614,10 +616,8 @@ public class XMPPConnection extends Connection { */ private void initConnection() throws XMPPException { boolean isFirstInitialization = packetReader == null || packetWriter == null; - if (!isFirstInitialization) { - compressionHandler = null; - serverAckdCompression = false; - } + compressionHandler = null; + serverAckdCompression = false; // Set the reader and writer instance variables initReaderAndWriter(); @@ -661,7 +661,7 @@ public class XMPPConnection extends Connection { } } else if (!wasAuthenticated) { - packetReader.notifyReconnection(); + notifyReconnection(); } } @@ -773,7 +773,7 @@ public class XMPPConnection extends Connection { void startTLSReceived(boolean required) { if (required && config.getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) { - packetReader.notifyConnectionError(new IllegalStateException( + notifyConnectionError(new IllegalStateException( "TLS required by server but not allowed by connection configuration")); return; } @@ -787,7 +787,7 @@ public class XMPPConnection extends Connection { writer.flush(); } catch (IOException e) { - packetReader.notifyConnectionError(e); + notifyConnectionError(e); } } @@ -977,7 +977,7 @@ public class XMPPConnection extends Connection { writer.flush(); } catch (IOException e) { - packetReader.notifyConnectionError(e); + notifyConnectionError(e); } } @@ -1041,7 +1041,7 @@ public class XMPPConnection extends Connection { login(config.getUsername(), config.getPassword(), config.getResource()); } - packetReader.notifyReconnection(); + notifyReconnection(); } catch (XMPPException e) { e.printStackTrace(); @@ -1059,4 +1059,50 @@ public class XMPPConnection extends Connection { this.wasAuthenticated = wasAuthenticated; } } + + /** + * 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/test/org/jivesoftware/smack/ReconnectionTest.java b/test/org/jivesoftware/smack/ReconnectionTest.java index f4cbd0c32..35c085813 100644 --- a/test/org/jivesoftware/smack/ReconnectionTest.java +++ b/test/org/jivesoftware/smack/ReconnectionTest.java @@ -44,7 +44,7 @@ public class ReconnectionTest extends SmackTestCase { connection.addConnectionListener(listener); // Simulates an error in the connection - connection.packetReader.notifyConnectionError(new Exception("Simulated Error")); + connection.notifyConnectionError(new Exception("Simulated Error")); Thread.sleep(12000); // After 10 seconds, the reconnection manager must reestablishes the connection assertEquals("The ConnectionListener.connectionStablished() notification was not fired", @@ -79,7 +79,7 @@ public class ReconnectionTest extends SmackTestCase { connection.addConnectionListener(listener); // Simulates an error in the connection - connection.packetReader.notifyConnectionError(new Exception("Simulated Error")); + connection.notifyConnectionError(new Exception("Simulated Error")); Thread.sleep(12000); // After 10 seconds, the reconnection manager must reestablishes the connection assertEquals("The ConnectionListener.connectionStablished() notification was not fired", @@ -103,7 +103,7 @@ public class ReconnectionTest extends SmackTestCase { connection.addConnectionListener(listener); // Produces a connection error - connection.packetReader.notifyConnectionError(new Exception("Simulated Error")); + connection.notifyConnectionError(new Exception("Simulated Error")); assertEquals( "An error occurs but the ConnectionListener.connectionClosedOnError(e) was not notified", true, listener.connectionClosedOnError); diff --git a/test/org/jivesoftware/smack/RosterSmackTest.java b/test/org/jivesoftware/smack/RosterSmackTest.java index 6436f69f4..8bee8124f 100644 --- a/test/org/jivesoftware/smack/RosterSmackTest.java +++ b/test/org/jivesoftware/smack/RosterSmackTest.java @@ -689,7 +689,7 @@ public class RosterSmackTest extends SmackTestCase { Thread.sleep(200); // Break the connection - getConnection(0).packetReader.notifyConnectionError(new Exception("Simulated Error")); + getConnection(0).notifyConnectionError(new Exception("Simulated Error")); Presence presence = roster.getPresence(getBareJID(1)); assertFalse("Unavailable presence not found for offline user", presence.isAvailable()); From aab1dcdabee48b90b0d175fd99167afe92a28534 Mon Sep 17 00:00:00 2001 From: rcollier Date: Tue, 19 Mar 2013 02:37:36 +0000 Subject: [PATCH 09/41] SMACK-412 Split the ping implementation to a server ping to replace keepalive and a simplified ping manager for manual pings of other entities. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13569 b35dd754-fafc-0310-a699-88a17e54d16e --- build/resources/META-INF/smack-config.xml | 36 +- build/resources/META-INF/smack.providers | 2 +- source/org/jivesoftware/smack/Connection.java | 4 +- .../org/jivesoftware/smack/PacketWriter.java | 82 +---- .../smack/SmackConfiguration.java | 26 -- .../jivesoftware/smack/XMPPConnection.java | 4 - .../ping/PingFailedListener.java | 8 +- .../smack/ping/ServerPingManager.java | 285 +++++++++++++++ .../{smackx => smack}/ping/packet/Ping.java | 13 +- .../ping/provider/PingProvider.java | 4 +- .../smack/util/SyncPacketSend.java | 2 +- .../jivesoftware/smackx/ping/PingManager.java | 334 +++--------------- .../smackx/ping/ServerPingTask.java | 77 ---- .../jivesoftware/smackx/ping/packet/Pong.java | 45 --- .../jivesoftware/smack/DummyConnection.java | 26 +- .../smack/ThreadedDummyConnection.java | 159 +++++---- .../smack/ping/ServerPingTest.java | 162 +++++++++ .../smackx/ping/PingPongTest.java | 127 ++++++- .../smackx/pubsub/ConfigureFormTest.java | 4 +- 19 files changed, 749 insertions(+), 651 deletions(-) rename source/org/jivesoftware/{smackx => smack}/ping/PingFailedListener.java (80%) create mode 100644 source/org/jivesoftware/smack/ping/ServerPingManager.java rename source/org/jivesoftware/{smackx => smack}/ping/packet/Ping.java (77%) rename source/org/jivesoftware/{smackx => smack}/ping/provider/PingProvider.java (91%) delete mode 100644 source/org/jivesoftware/smackx/ping/ServerPingTask.java delete mode 100644 source/org/jivesoftware/smackx/ping/packet/Pong.java create mode 100644 test-unit/org/jivesoftware/smack/ping/ServerPingTest.java diff --git a/build/resources/META-INF/smack-config.xml b/build/resources/META-INF/smack-config.xml index 1c763e0c2..7009ab468 100644 --- a/build/resources/META-INF/smack-config.xml +++ b/build/resources/META-INF/smack-config.xml @@ -2,21 +2,6 @@ - - - org.jivesoftware.smackx.ServiceDiscoveryManager - org.jivesoftware.smack.PrivacyListManager - org.jivesoftware.smackx.XHTMLManager - org.jivesoftware.smackx.muc.MultiUserChat - org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager - org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager - org.jivesoftware.smackx.filetransfer.FileTransferManager - org.jivesoftware.smackx.LastActivityManager - org.jivesoftware.smack.ReconnectionManager - org.jivesoftware.smackx.commands.AdHocCommandManager - org.jivesoftware.smack.util.dns.JavaxResolver - - 5000 @@ -31,11 +16,24 @@ 10000 - - - 1800 - + false + + + + org.jivesoftware.smackx.ServiceDiscoveryManager + org.jivesoftware.smack.PrivacyListManager + org.jivesoftware.smack.ping.ServerPingManager + org.jivesoftware.smackx.XHTMLManager + org.jivesoftware.smackx.muc.MultiUserChat + org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager + org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager + org.jivesoftware.smackx.filetransfer.FileTransferManager + org.jivesoftware.smackx.LastActivityManager + org.jivesoftware.smack.ReconnectionManager + org.jivesoftware.smackx.commands.AdHocCommandManager + + diff --git a/build/resources/META-INF/smack.providers b/build/resources/META-INF/smack.providers index f1a7c1f91..28be01c7b 100644 --- a/build/resources/META-INF/smack.providers +++ b/build/resources/META-INF/smack.providers @@ -241,7 +241,7 @@ ping urn:xmpp:ping - org.jivesoftware.smackx.ping.provider.PingProvider + org.jivesoftware.smack.ping.provider.PingProvider diff --git a/source/org/jivesoftware/smack/Connection.java b/source/org/jivesoftware/smack/Connection.java index 9a213dbc8..ced103f22 100644 --- a/source/org/jivesoftware/smack/Connection.java +++ b/source/org/jivesoftware/smack/Connection.java @@ -178,12 +178,12 @@ public abstract class Connection { protected SmackDebugger debugger = null; /** - * The Reader which is used for the {@see debugger}. + * The Reader which is used for the debugger. */ protected Reader reader; /** - * The Writer which is used for the {@see debugger}. + * The Writer which is used for the debugger. */ protected Writer writer; diff --git a/source/org/jivesoftware/smack/PacketWriter.java b/source/org/jivesoftware/smack/PacketWriter.java index f8b5c359f..559e2afd9 100644 --- a/source/org/jivesoftware/smack/PacketWriter.java +++ b/source/org/jivesoftware/smack/PacketWriter.java @@ -40,18 +40,11 @@ import java.util.concurrent.BlockingQueue; class PacketWriter { private Thread writerThread; - private Thread keepAliveThread; private Writer writer; private XMPPConnection connection; private final BlockingQueue queue; volatile boolean done; - /** - * Timestamp when the last stanza was sent to the server. This information is used - * by the keep alive process to only send heartbeats when the connection has been idle. - */ - private long lastActive = System.currentTimeMillis(); - /** * Creates a new packet writer with the specified connection. * @@ -117,25 +110,6 @@ class PacketWriter { writerThread.start(); } - /** - * Starts the keep alive process. A white space (aka heartbeat) is going to be - * sent to the server every 30 seconds (by default) since the last stanza was sent - * to the server. - */ - void startKeepAliveProcess() { - // Schedule a keep-alive task to run if the feature is enabled. will write - // out a space character each time it runs to keep the TCP/IP connection open. - int keepAliveInterval = SmackConfiguration.getKeepAliveInterval(); - if (keepAliveInterval > 0) { - KeepAliveTask task = new KeepAliveTask(keepAliveInterval); - keepAliveThread = new Thread(task); - task.setThread(keepAliveThread); - keepAliveThread.setDaemon(true); - keepAliveThread.setName("Smack Keep Alive (" + connection.connectionCounterValue + ")"); - keepAliveThread.start(); - } - } - void setWriter(Writer writer) { this.writer = writer; } @@ -149,9 +123,6 @@ class PacketWriter { synchronized (queue) { queue.notifyAll(); } - // Interrupt the keep alive thread if one was created - if (keepAliveThread != null) - keepAliveThread.interrupt(); } /** @@ -193,10 +164,9 @@ class PacketWriter { if (packet != null) { synchronized (writer) { writer.write(packet.toXML()); + writer.flush(); if (queue.isEmpty()) { writer.flush(); - // Keep track of the last time a stanza was sent to the server - lastActive = System.currentTimeMillis(); } } } @@ -268,54 +238,4 @@ class PacketWriter { writer.write(stream.toString()); writer.flush(); } - - /** - * A TimerTask that keeps connections to the server alive by sending a space - * character on an interval. - */ - private class KeepAliveTask implements Runnable { - - private int delay; - private Thread thread; - - public KeepAliveTask(int delay) { - this.delay = delay; - } - - protected void setThread(Thread thread) { - this.thread = thread; - } - - public void run() { - try { - // Sleep a minimum of 15 seconds plus delay before sending first heartbeat. This will give time to - // properly finish TLS negotiation and then start sending heartbeats. - Thread.sleep(15000 + delay); - } - catch (InterruptedException ie) { - // Do nothing - } - while (!done && keepAliveThread == thread) { - synchronized (writer) { - // Send heartbeat if no packet has been sent to the server for a given time - if (System.currentTimeMillis() - lastActive >= delay) { - try { - writer.write(" "); - writer.flush(); - } - catch (Exception e) { - // Do nothing - } - } - } - try { - // Sleep until we should write the next keep-alive. - Thread.sleep(delay); - } - catch (InterruptedException ie) { - // Do nothing - } - } - } - } } diff --git a/source/org/jivesoftware/smack/SmackConfiguration.java b/source/org/jivesoftware/smack/SmackConfiguration.java index 80f1906af..fcb1d4beb 100644 --- a/source/org/jivesoftware/smack/SmackConfiguration.java +++ b/source/org/jivesoftware/smack/SmackConfiguration.java @@ -58,11 +58,6 @@ public final class SmackConfiguration { 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 */ @@ -117,9 +112,6 @@ public final class SmackConfiguration { 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()); } @@ -319,24 +311,6 @@ public final class SmackConfiguration { 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 diff --git a/source/org/jivesoftware/smack/XMPPConnection.java b/source/org/jivesoftware/smack/XMPPConnection.java index 6512aa981..2c75e208a 100644 --- a/source/org/jivesoftware/smack/XMPPConnection.java +++ b/source/org/jivesoftware/smack/XMPPConnection.java @@ -650,10 +650,6 @@ public class XMPPConnection extends Connection { // Make note of the fact that we're now connected. connected = true; - // Start keep alive process (after TLS was negotiated - if available) - packetWriter.startKeepAliveProcess(); - - if (isFirstInitialization) { // Notify listeners that a new connection has been established for (ConnectionCreationListener listener : getConnectionCreationListeners()) { diff --git a/source/org/jivesoftware/smackx/ping/PingFailedListener.java b/source/org/jivesoftware/smack/ping/PingFailedListener.java similarity index 80% rename from source/org/jivesoftware/smackx/ping/PingFailedListener.java rename to source/org/jivesoftware/smack/ping/PingFailedListener.java index 4cda33b0c..450345773 100644 --- a/source/org/jivesoftware/smackx/ping/PingFailedListener.java +++ b/source/org/jivesoftware/smack/ping/PingFailedListener.java @@ -14,8 +14,14 @@ * limitations under the License. */ -package org.jivesoftware.smackx.ping; +package org.jivesoftware.smack.ping; +/** + * Defines the callback used whenever the server ping fails. + */ public interface PingFailedListener { + /** + * Called when the server ping fails. + */ void pingFailed(); } \ No newline at end of file diff --git a/source/org/jivesoftware/smack/ping/ServerPingManager.java b/source/org/jivesoftware/smack/ping/ServerPingManager.java new file mode 100644 index 000000000..171710bdf --- /dev/null +++ b/source/org/jivesoftware/smack/ping/ServerPingManager.java @@ -0,0 +1,285 @@ +/** + * 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.smack.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.ThreadFactory; +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.AndFilter; +import org.jivesoftware.smack.filter.IQTypeFilter; +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.IQ.Type; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.ping.packet.Ping; +import org.jivesoftware.smackx.ServiceDiscoveryManager; + +/** + * Using an implementation of XMPP Ping (XEP-0199). This + * class provides keepalive functionality with the server that will periodically "ping" the server to maintain and/or + * verify that the connection still exists. + *

+ * The ping is done at the application level and is therefore protocol agnostic. It will thus work for both standard TCP + * connections as well as BOSH or any other transport protocol. It will also work regardless of whether the server + * supports the Ping extension, since an error response to the ping serves the same purpose as a pong. + * + * @author Florian Schmaus + */ +public class ServerPingManager { + public static final long PING_MINIMUM = 10000; + + private static Map instances = Collections + .synchronizedMap(new WeakHashMap()); + private static long defaultPingInterval = SmackConfiguration.getKeepAliveInterval(); + + private static ScheduledExecutorService periodicPingExecutorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() { + @Override + public Thread newThread(Runnable runnable) { + Thread pingThread = new Thread(runnable, "Smack Server Ping"); + pingThread.setDaemon(true); + return pingThread; + } + }); + + static { + if (defaultPingInterval > 0) { + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(Connection connection) { + new ServerPingManager(connection); + } + }); + } + } + + private Connection connection; + private long pingInterval = SmackConfiguration.getKeepAliveInterval(); + private Set pingFailedListeners = Collections.synchronizedSet(new HashSet()); + private volatile ScheduledFuture periodicPingTask; + private volatile long lastSuccessfulContact = -1; + + /** + * Retrieves a {@link ServerPingManager} for the specified {@link Connection}, creating one if it doesn't already + * exist. + * + * @param connection + * The connection the manager is attached to. + * @return The new or existing manager. + */ + public synchronized static ServerPingManager getInstanceFor(Connection connection) { + ServerPingManager pingManager = instances.get(connection); + + if (pingManager == null) { + pingManager = new ServerPingManager(connection); + } + return pingManager; + } + + private ServerPingManager(Connection connection) { + ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); + sdm.addFeature(Ping.NAMESPACE); + this.connection = connection; + init(); + } + + private void init() { + PacketFilter pingPacketFilter = new AndFilter(new PacketTypeFilter(Ping.class), new IQTypeFilter(Type.GET)); + + connection.addPacketListener(new PacketListener() { + /** + * Sends a Pong for every Ping + */ + public void processPacket(Packet packet) { + IQ pong = IQ.createResultIQ((Ping) packet); + connection.sendPacket(pong); + } + }, pingPacketFilter); + + connection.addConnectionListener(new ConnectionListener() { + + @Override + public void connectionClosed() { + stopPingServerTask(); + } + + @Override + public void connectionClosedOnError(Exception arg0) { + stopPingServerTask(); + } + + @Override + public void reconnectionSuccessful() { + schedulePingServerTask(); + } + + @Override + public void reconnectingIn(int seconds) { + } + + @Override + public void reconnectionFailed(Exception e) { + } + }); + + // Listen for all incoming packets and reset the scheduled ping whenever + // one arrives. + connection.addPacketListener(new PacketListener() { + + @Override + public void processPacket(Packet packet) { + // reschedule the ping based on this last server contact + lastSuccessfulContact = System.currentTimeMillis(); + schedulePingServerTask(); + } + }, null); + instances.put(connection, this); + schedulePingServerTask(); + } + + /** + * Sets the ping interval. + * + * @param pingInterval + * The new ping time interval in milliseconds. + */ + public void setPingInterval(long newPingInterval) { + if (newPingInterval < PING_MINIMUM) + newPingInterval = PING_MINIMUM; + + if (pingInterval != newPingInterval) { + pingInterval = newPingInterval; + schedulePingServerTask(); + } + } + + /** + * Stops pinging the server. This cannot stop a ping that has already started, but will prevent another from being triggered. + *

+ * To restart, call {@link #setPingInterval(long)}. + */ + public void stopPinging() { + pingInterval = -1; + stopPingServerTask(); + } + + /** + * Gets the ping interval. + * + * @return The ping interval in milliseconds. + */ + public long getPingInterval() { + return pingInterval; + } + + /** + * Add listener for notification when a server ping fails. + * + *

+ * Please note that this doesn't necessarily mean that the connection is lost, a slow to respond server could also + * cause a failure due to taking too long to respond and thus causing a reply timeout. + * + * @param listener + * The listener to be called + */ + public void addPingFailedListener(PingFailedListener listener) { + pingFailedListeners.add(listener); + } + + /** + * Remove the listener. + * + * @param listener + * The listener to be removed. + */ + public void removePingFailedListener(PingFailedListener listener) { + pingFailedListeners.remove(listener); + } + + /** + * Returns the time of the last successful contact with the server. (i.e. the last time any message was received). + * + * @return Time of last message or -1 if none has been received since manager was created. + */ + public long getLastSuccessfulContact() { + return lastSuccessfulContact; + } + + /** + * Cancels any existing periodic ping task if there is one and schedules a new ping task if pingInterval is greater + * then zero. + * + * This is designed so only one executor is used for scheduling all pings on all connections. This results in only 1 thread used for pinging. + */ + private synchronized void schedulePingServerTask() { + stopPingServerTask(); + + if (pingInterval > 0) { + periodicPingTask = periodicPingExecutorService.schedule(new Runnable() { + @Override + public void run() { + Ping ping = new Ping(); + PacketFilter responseFilter = new PacketIDFilter(ping.getPacketID()); + final PacketCollector response = connection.createPacketCollector(responseFilter); + connection.sendPacket(ping); + + if (!pingFailedListeners.isEmpty()) { + // Schedule a collector for the ping reply, notify listeners if none is received. + periodicPingExecutorService.schedule(new Runnable() { + @Override + public void run() { + Packet result = response.nextResult(1); + + // Stop queuing results + response.cancel(); + + // The actual result of the reply can be ignored since we only care if we actually got one. + if (result == null) { + for (PingFailedListener listener : pingFailedListeners) { + listener.pingFailed(); + } + } + } + }, SmackConfiguration.getPacketReplyTimeout(), TimeUnit.MILLISECONDS); + } + } + }, getPingInterval(), TimeUnit.MILLISECONDS); + } + } + + private void stopPingServerTask() { + if (periodicPingTask != null) { + periodicPingTask.cancel(true); + periodicPingTask = null; + } + } +} diff --git a/source/org/jivesoftware/smackx/ping/packet/Ping.java b/source/org/jivesoftware/smack/ping/packet/Ping.java similarity index 77% rename from source/org/jivesoftware/smackx/ping/packet/Ping.java rename to source/org/jivesoftware/smack/ping/packet/Ping.java index fc5bbdfdc..e2db68153 100644 --- a/source/org/jivesoftware/smackx/ping/packet/Ping.java +++ b/source/org/jivesoftware/smack/ping/packet/Ping.java @@ -14,25 +14,26 @@ * limitations under the License. */ -package org.jivesoftware.smackx.ping.packet; +package org.jivesoftware.smack.ping.packet; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smackx.ping.PingManager; public class Ping extends IQ { + + public static final String NAMESPACE = "urn:xmpp:ping"; + public static final String ELEMENT = "ping"; public Ping() { } - public Ping(String from, String to) { + public Ping(String to) { setTo(to); - setFrom(from); setType(IQ.Type.GET); - setPacketID(getPacketID()); } + @Override public String getChildElementXML() { - return "<" + PingManager.ELEMENT + " xmlns=\'" + PingManager.NAMESPACE + "\' />"; + return "<" + ELEMENT + " xmlns=\'" + NAMESPACE + "\' />"; } - } diff --git a/source/org/jivesoftware/smackx/ping/provider/PingProvider.java b/source/org/jivesoftware/smack/ping/provider/PingProvider.java similarity index 91% rename from source/org/jivesoftware/smackx/ping/provider/PingProvider.java rename to source/org/jivesoftware/smack/ping/provider/PingProvider.java index ebe766953..a55f1cc04 100644 --- a/source/org/jivesoftware/smackx/ping/provider/PingProvider.java +++ b/source/org/jivesoftware/smack/ping/provider/PingProvider.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.jivesoftware.smackx.ping.provider; +package org.jivesoftware.smack.ping.provider; import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.ping.packet.Ping; import org.jivesoftware.smack.provider.IQProvider; -import org.jivesoftware.smackx.ping.packet.Ping; import org.xmlpull.v1.XmlPullParser; public class PingProvider implements IQProvider { diff --git a/source/org/jivesoftware/smack/util/SyncPacketSend.java b/source/org/jivesoftware/smack/util/SyncPacketSend.java index a1c238aeb..d658637f0 100644 --- a/source/org/jivesoftware/smack/util/SyncPacketSend.java +++ b/source/org/jivesoftware/smack/util/SyncPacketSend.java @@ -47,7 +47,7 @@ final public class SyncPacketSend response.cancel(); if (result == null) { - throw new XMPPException("No response from server."); + throw new XMPPException("No response from " + packet.getTo()); } else if (result.getError() != null) { throw new XMPPException(result.getError()); diff --git a/source/org/jivesoftware/smackx/ping/PingManager.java b/source/org/jivesoftware/smackx/ping/PingManager.java index 3ee79a3f5..7e7e9d2cf 100644 --- a/source/org/jivesoftware/smackx/ping/PingManager.java +++ b/source/org/jivesoftware/smackx/ping/PingManager.java @@ -16,328 +16,80 @@ 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.smack.ping.ServerPingManager; +import org.jivesoftware.smack.ping.packet.Ping; +import org.jivesoftware.smack.util.SyncPacketSend; 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. + * Implements the XMPP Ping as defined by XEP-0199. The XMPP Ping protocol + * allows one entity to 'ping' any other entity by simply sending a ping to + * the appropriate JID. + *

+ * NOTE: The {@link ServerPingManager} already provides a keepalive functionality + * for regularly pinging the server to keep the underlying transport connection + * alive. This class is specifically intended to do manual pings of other + * entities. * * @author Florian Schmaus * @see XEP-0199:XMPP * Ping */ public class PingManager { - - public static final String NAMESPACE = "urn:xmpp:ping"; - public static final String ELEMENT = "ping"; - - - private static Map instances = - Collections.synchronizedMap(new WeakHashMap()); - - 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 pingFailedListeners = Collections - .synchronizedSet(new HashSet()); - 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) { + public PingManager(Connection connection) { ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); - sdm.addFeature(NAMESPACE); + sdm.addFeature(Ping.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); - } + /** + * Pings the given jid. This method will return false if an error occurs. + *

+ * Use {@link #isPingSupported(String)} to determine if XMPP Ping is supported + * by the entity. + * + * @param jid The id of the entity the ping is being sent to + * @param pingTimeout The time to wait for a reply + * @return true if a reply was received from the entity, false otherwise. + */ + public boolean ping(String jid, long pingTimeout) { + Ping ping = new Ping(jid); + + try { + SyncPacketSend.getReply(connection, ping); } - , 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); + catch (XMPPException exc) { + return false; } - - 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; + return true; } /** - * 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. + * Same as calling {@link #ping(String, long)} with the defaultpacket reply + * timeout. * - * You should use isPingSupported(jid) to determine if XMPP Ping is - * supported by the user. - * - * @param jid - * @param pingTimeout - * @return + * @param jid The id of the entity the ping is being sent to + * @return true if a reply was received from the entity, false otherwise. */ - 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) { + public boolean ping(String jid) { return ping(jid, SmackConfiguration.getPacketReplyTimeout()); } /** - * Pings the given Entity. + * Query the specified entity to see if it supports the Ping protocol (XEP-0199) * - * 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 + * @param jid The id of the entity the query is being sent to + * @return true if it supports ping, false otherwise. + * @throws XMPPException An XMPP related error occurred during the request */ - 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 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(); + public boolean isPingSupported(String jid) throws XMPPException { + DiscoverInfo result = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid); + return result.containsFeature(Ping.NAMESPACE); } } diff --git a/source/org/jivesoftware/smackx/ping/ServerPingTask.java b/source/org/jivesoftware/smackx/ping/ServerPingTask.java deleted file mode 100644 index 0901b8f15..000000000 --- a/source/org/jivesoftware/smackx/ping/ServerPingTask.java +++ /dev/null @@ -1,77 +0,0 @@ -/** - * 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 weakConnection; - - private int delta = 1000; // 1 seconds - private int tries = 3; // 3 tries - - protected ServerPingTask(Connection connection) { - this.weakConnection = new WeakReference(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 pingFailedListeners = pingManager.getPingFailedListeners(); - for (PingFailedListener l : pingFailedListeners) { - l.pingFailed(); - } - } else { - // Ping was successful, wind-up the periodic task again - pingManager.maybeSchedulePingServerTask(); - } - } - } -} diff --git a/source/org/jivesoftware/smackx/ping/packet/Pong.java b/source/org/jivesoftware/smackx/ping/packet/Pong.java deleted file mode 100644 index 9300db0ce..000000000 --- a/source/org/jivesoftware/smackx/ping/packet/Pong.java +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 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/test-unit/org/jivesoftware/smack/DummyConnection.java b/test-unit/org/jivesoftware/smack/DummyConnection.java index 0c4b75c1e..c144eff5a 100644 --- a/test-unit/org/jivesoftware/smack/DummyConnection.java +++ b/test-unit/org/jivesoftware/smack/DummyConnection.java @@ -57,11 +57,15 @@ public class DummyConnection extends Connection { private final BlockingQueue queue = new LinkedBlockingQueue(); public DummyConnection() { - super(new ConnectionConfiguration("example.com")); + this(new ConnectionConfiguration("example.com")); } public DummyConnection(ConnectionConfiguration configuration) { super(configuration); + + for (ConnectionCreationListener listener : getConnectionCreationListeners()) { + listener.connectionCreated(this); + } } @Override @@ -191,15 +195,27 @@ public class DummyConnection extends Connection { } /** - * Returns the first packet that's sent through {@link #sendPacket(Packet)} and - * that has not been returned by earlier calls to this method. This method - * will block for up to two seconds if no packets have been sent yet. + * Returns the first packet that's sent through {@link #sendPacket(Packet)} + * and that has not been returned by earlier calls to this method. * * @return a sent packet. * @throws InterruptedException */ public Packet getSentPacket() throws InterruptedException { - return queue.poll(2, TimeUnit.SECONDS); + return queue.poll(); + } + + /** + * Returns the first packet that's sent through {@link #sendPacket(Packet)} + * and that has not been returned by earlier calls to this method. This + * method will block for up to the specified number of seconds if no packets + * have been sent yet. + * + * @return a sent packet. + * @throws InterruptedException + */ + public Packet getSentPacket(int wait) throws InterruptedException { + return queue.poll(wait, TimeUnit.SECONDS); } /** diff --git a/test-unit/org/jivesoftware/smack/ThreadedDummyConnection.java b/test-unit/org/jivesoftware/smack/ThreadedDummyConnection.java index 3375801db..daf5fc14c 100644 --- a/test-unit/org/jivesoftware/smack/ThreadedDummyConnection.java +++ b/test-unit/org/jivesoftware/smack/ThreadedDummyConnection.java @@ -17,80 +17,85 @@ * 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.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -import org.jivesoftware.smack.packet.IQ; -import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smack.packet.Packet; -import org.jivesoftware.smack.packet.IQ.Type; - -public class ThreadedDummyConnection extends DummyConnection -{ - private BlockingQueue replyQ = new ArrayBlockingQueue(1); - private BlockingQueue messageQ = new LinkedBlockingQueue(5); - - @Override - public void sendPacket(Packet packet) - { - super.sendPacket(packet); - - if ((packet instanceof IQ) && !replyQ.isEmpty()) - { - // Set reply packet to match one being sent. We haven't started the - // other thread yet so this is still safe. - IQ replyPacket = replyQ.peek(); - replyPacket.setPacketID(packet.getPacketID()); - replyPacket.setFrom(packet.getTo()); - replyPacket.setTo(packet.getFrom()); - replyPacket.setType(Type.RESULT); - - new ProcessQueue(replyQ).start(); - } - } - - public void addMessage(Message msgToProcess) - { - messageQ.add(msgToProcess); - } - - public void addIQReply(IQ reply) - { - replyQ.add(reply); - } - - public void processMessages() - { - if (!messageQ.isEmpty()) - new ProcessQueue(messageQ).start(); - else - System.out.println("No messages to process"); - } - - class ProcessQueue extends Thread - { - private BlockingQueue processQ; - - ProcessQueue(BlockingQueue queue) - { - processQ = queue; - } - - @Override - public void run() - { - try - { - processPacket(processQ.take()); - } - catch (InterruptedException e) - { - e.printStackTrace(); - } - } - }; - -} +package org.jivesoftware.smack; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.IQ.Type; + +public class ThreadedDummyConnection extends DummyConnection { + private BlockingQueue replyQ = new ArrayBlockingQueue(1); + private BlockingQueue messageQ = new LinkedBlockingQueue(5); + private volatile boolean timeout = false; + + @Override + public void sendPacket(Packet packet) { + super.sendPacket(packet); + + if (packet instanceof IQ && !timeout) { + timeout = false; + // Set reply packet to match one being sent. We haven't started the + // other thread yet so this is still safe. + IQ replyPacket = replyQ.peek(); + + // If no reply has been set via addIQReply, then we create a simple reply + if (replyPacket == null) { + replyPacket = IQ.createResultIQ((IQ) packet); + replyQ.add(replyPacket); + } + replyPacket.setPacketID(packet.getPacketID()); + replyPacket.setFrom(packet.getTo()); + replyPacket.setTo(packet.getFrom()); + replyPacket.setType(Type.RESULT); + + new ProcessQueue(replyQ).start(); + } + } + + /** + * Calling this method will cause the next sendPacket call with an IQ packet to timeout. + * This is accomplished by simply stopping the auto creating of the reply packet + * or processing one that was entered via {@link #processPacket(Packet)}. + */ + public void setTimeout() { + timeout = true; + } + + public void addMessage(Message msgToProcess) { + messageQ.add(msgToProcess); + } + + public void addIQReply(IQ reply) { + replyQ.add(reply); + } + + public void processMessages() { + if (!messageQ.isEmpty()) + new ProcessQueue(messageQ).start(); + else + System.out.println("No messages to process"); + } + + class ProcessQueue extends Thread { + private BlockingQueue processQ; + + ProcessQueue(BlockingQueue queue) { + processQ = queue; + } + + @Override + public void run() { + try { + processPacket(processQ.take()); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + +} diff --git a/test-unit/org/jivesoftware/smack/ping/ServerPingTest.java b/test-unit/org/jivesoftware/smack/ping/ServerPingTest.java new file mode 100644 index 000000000..fa16dce03 --- /dev/null +++ b/test-unit/org/jivesoftware/smack/ping/ServerPingTest.java @@ -0,0 +1,162 @@ +package org.jivesoftware.smack.ping; + +import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.DummyConnection; +import org.jivesoftware.smack.PacketInterceptor; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.TestUtils; +import org.jivesoftware.smack.ThreadedDummyConnection; +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.ping.packet.Ping; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.junit.Test; + +public class ServerPingTest { + private static String TO = "juliet@capulet.lit/balcony"; + private static String ID = "s2c1"; + + private static Properties outputProperties = new Properties(); + { + outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); + } + + /* + * Stanza copied from spec + */ + @Test + public void validatePingStanzaXML() throws Exception { + // @formatter:off + String control = "" + + "" + ""; + // @formatter:on + + Ping ping = new Ping(TO); + ping.setPacketID(ID); + + assertXMLEqual(control, ping.toXML()); + } + + @Test + public void checkProvider() throws Exception { + // @formatter:off + String control = "" + + "" + ""; + // @formatter:on + DummyConnection con = new DummyConnection(); + IQ pingRequest = PacketParserUtils.parseIQ(TestUtils.getIQParser(control), con); + + assertTrue(pingRequest instanceof Ping); + + con.processPacket(pingRequest); + + Packet pongPacket = con.getSentPacket(); + assertTrue(pongPacket instanceof IQ); + + IQ pong = (IQ) pongPacket; + assertEquals("juliet@capulet.lit/balcony", pong.getFrom()); + assertEquals("capulet.lit", pong.getTo()); + assertEquals("s2c1", pong.getPacketID()); + assertEquals(IQ.Type.RESULT, pong.getType()); + } + + @Test + public void serverPingFailSingleConnection() throws Exception { + DummyConnection connection = getConnection(); + CountDownLatch latch = new CountDownLatch(2); + addInterceptor(connection, latch); + addPingFailedListener(connection, latch); + + // Time based testing kind of sucks, but this should be reliable on a DummyConnection since there + // is no actual server involved. This will provide enough time to ping and wait for the lack of response. + assertTrue(latch.await(getWaitTime(), TimeUnit.MILLISECONDS)); + } + + @Test + public void serverPingSuccessfulSingleConnection() throws Exception { + ThreadedDummyConnection connection = getThreadedConnection(); + final CountDownLatch latch = new CountDownLatch(1); + + connection.addPacketListener(new PacketListener() { + @Override + public void processPacket(Packet packet) { + latch.countDown(); + } + }, new IQTypeFilter(IQ.Type.RESULT)); + + // Time based testing kind of sucks, but this should be reliable on a DummyConnection since there + // is no actual server involved. This will provide enough time to ping and wait for the lack of response. + assertTrue(latch.await(getWaitTime(), TimeUnit.MILLISECONDS)); + } + + @Test + public void serverPingFailMultipleConnection() throws Exception { + CountDownLatch latch = new CountDownLatch(6); + SmackConfiguration.setPacketReplyTimeout(15000); + + DummyConnection con1 = getConnection(); + addInterceptor(con1, latch); + addPingFailedListener(con1, latch); + + DummyConnection con2 = getConnection(); + addInterceptor(con2, latch); + addPingFailedListener(con2, latch); + + DummyConnection con3 = getConnection(); + addInterceptor(con3, latch); + addPingFailedListener(con2, latch); + + assertTrue(latch.await(getWaitTime(), TimeUnit.MILLISECONDS)); + } + + private void addPingFailedListener(DummyConnection con, final CountDownLatch latch) { + ServerPingManager manager = ServerPingManager.getInstanceFor(con); + manager.addPingFailedListener(new PingFailedListener() { + @Override + public void pingFailed() { + latch.countDown(); + } + }); + } + + private DummyConnection getConnection() { + DummyConnection con = new DummyConnection(); + ServerPingManager mgr = ServerPingManager.getInstanceFor(con); + mgr.setPingInterval(ServerPingManager.PING_MINIMUM); + + return con; + } + + private ThreadedDummyConnection getThreadedConnection() { + ThreadedDummyConnection con = new ThreadedDummyConnection(); + ServerPingManager mgr = ServerPingManager.getInstanceFor(con); + mgr.setPingInterval(ServerPingManager.PING_MINIMUM); + + return con; + } + + private void addInterceptor(final Connection con, final CountDownLatch latch) { + con.addPacketInterceptor(new PacketInterceptor() { + @Override + public void interceptPacket(Packet packet) { + con.removePacketInterceptor(this); + latch.countDown(); + } + }, new PacketTypeFilter(Ping.class)); + } + + private long getWaitTime() { + return ServerPingManager.PING_MINIMUM + SmackConfiguration.getPacketReplyTimeout() + 3000; + } +} diff --git a/test-unit/org/jivesoftware/smackx/ping/PingPongTest.java b/test-unit/org/jivesoftware/smackx/ping/PingPongTest.java index 762223ee4..0346bb034 100644 --- a/test-unit/org/jivesoftware/smackx/ping/PingPongTest.java +++ b/test-unit/org/jivesoftware/smackx/ping/PingPongTest.java @@ -15,24 +15,127 @@ */ package org.jivesoftware.smackx.ping; -import static org.junit.Assert.assertEquals; - -import org.jivesoftware.smackx.ping.packet.Ping; -import org.jivesoftware.smackx.ping.packet.Pong; +import org.jivesoftware.smack.DummyConnection; +import org.jivesoftware.smack.TestUtils; +import org.jivesoftware.smack.ThreadedDummyConnection; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.ping.packet.Ping; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.packet.DiscoverInfo; import org.junit.Test; +import static org.junit.Assert.*; + public class PingPongTest { @Test - public void createPongfromPingTest() { - Ping ping = new Ping("from@sender.local/resourceFrom", "to@receiver.local/resourceTo"); + public void checkSendingPing() throws Exception { + DummyConnection con = new DummyConnection(); + PingManager pinger = new PingManager(con); + pinger.ping("test@myserver.com"); - // create a pong from a ping - Pong pong = new Pong(ping); - - assertEquals(pong.getFrom(), ping.getTo()); - assertEquals(pong.getTo(), ping.getFrom()); - assertEquals(pong.getPacketID(), ping.getPacketID()); + Packet sentPacket = con.getSentPacket(); + + assertTrue(sentPacket instanceof Ping); + } + @Test + public void checkSuccessfulPing() throws Exception { + ThreadedDummyConnection con = new ThreadedDummyConnection(); + + PingManager pinger = new PingManager(con); + + boolean pingSuccess = pinger.ping("test@myserver.com"); + + assertTrue(pingSuccess); + + } + + /** + * DummyConnection will not reply so it will timeout. + * @throws Exception + */ + @Test + public void checkFailedPingOnTimeout() throws Exception { + DummyConnection con = new DummyConnection(); + PingManager pinger = new PingManager(con); + + boolean pingSuccess = pinger.ping("test@myserver.com"); + + assertFalse(pingSuccess); + + } + + /** + * DummyConnection will not reply so it will timeout. + * @throws Exception + */ + @Test + public void checkFailedPingError() throws Exception { + ThreadedDummyConnection con = new ThreadedDummyConnection(); + //@formatter:off + String reply = + "" + + "" + + "" + + "" + + "" + + ""; + //@formatter:on + IQ serviceUnavailable = PacketParserUtils.parseIQ(TestUtils.getIQParser(reply), con); + con.addIQReply(serviceUnavailable); + + PingManager pinger = new PingManager(con); + + boolean pingSuccess = pinger.ping("test@myserver.com"); + + assertFalse(pingSuccess); + + } + + @Test + public void checkSuccessfulDiscoRequest() throws Exception { + ThreadedDummyConnection con = new ThreadedDummyConnection(); + DiscoverInfo info = new DiscoverInfo(); + info.addFeature(Ping.NAMESPACE); + + //@formatter:off + String reply = + "" + + "" + + "" + + ""; + //@formatter:on + IQ discoReply = PacketParserUtils.parseIQ(TestUtils.getIQParser(reply), con); + con.addIQReply(discoReply); + + PingManager pinger = new PingManager(con); + boolean pingSupported = pinger.isPingSupported("test@myserver.com"); + + assertTrue(pingSupported); + } + + @Test + public void checkUnuccessfulDiscoRequest() throws Exception { + ThreadedDummyConnection con = new ThreadedDummyConnection(); + DiscoverInfo info = new DiscoverInfo(); + info.addFeature(Ping.NAMESPACE); + + //@formatter:off + String reply = + "" + + "" + + "" + + ""; + //@formatter:on + IQ discoReply = PacketParserUtils.parseIQ(TestUtils.getIQParser(reply), con); + con.addIQReply(discoReply); + + PingManager pinger = new PingManager(con); + boolean pingSupported = pinger.isPingSupported("test@myserver.com"); + + assertFalse(pingSupported); + } } diff --git a/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java b/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java index 4b47aa2ff..d3167bed4 100644 --- a/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java +++ b/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java @@ -83,7 +83,9 @@ public class ConfigureFormTest Node node = mgr.getNode("princely_musings"); - SmackConfiguration.setPacketReplyTimeout(100); + SmackConfiguration.setPacketReplyTimeout(100); + con.setTimeout(); + node.getNodeConfiguration(); } } From e3f842da8c85a5f8ee6eb55fccae49c288269d13 Mon Sep 17 00:00:00 2001 From: rcollier Date: Tue, 19 Mar 2013 22:42:43 +0000 Subject: [PATCH 10/41] SMACK-361 Updated test case to fix create temp directory on Windows git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13574 b35dd754-fafc-0310-a699-88a17e54d16e --- .../jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java b/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java index 754bf6d3c..5ae4642a9 100644 --- a/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java +++ b/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java @@ -216,9 +216,7 @@ public class EntityCapsManagerTest { } public static File createTempDirectory() throws IOException { - String tmpdir = System.getProperty("java.io.tmpdir"); - File tmp; - tmp = File.createTempFile(tmpdir, "entityCaps"); + File tmp = File.createTempFile("entity", "caps"); tmp.delete(); tmp.mkdir(); return tmp; From ff64c57b4dfa6d2ac81376ed03ef0635f088217d Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Fri, 22 Mar 2013 18:13:53 +0000 Subject: [PATCH 11/41] Added isFullJID() helper in StringUtils git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13575 b35dd754-fafc-0310-a699-88a17e54d16e --- .../org/jivesoftware/smack/util/StringUtils.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/source/org/jivesoftware/smack/util/StringUtils.java b/source/org/jivesoftware/smack/util/StringUtils.java index 39bc54945..7e3cfdc73 100644 --- a/source/org/jivesoftware/smack/util/StringUtils.java +++ b/source/org/jivesoftware/smack/util/StringUtils.java @@ -362,6 +362,20 @@ public class StringUtils { } } + /** + * 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, From b091f6161eb7881b3b7098349718b00c5590f0e3 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Fri, 22 Mar 2013 18:14:01 +0000 Subject: [PATCH 12/41] SMACK-430 Re-activated code that throws an exception if createOutgoingFileTransfer() was called with a bare JID git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13576 b35dd754-fafc-0310-a699-88a17e54d16e --- .../filetransfer/FileTransferManager.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/source/org/jivesoftware/smackx/filetransfer/FileTransferManager.java b/source/org/jivesoftware/smackx/filetransfer/FileTransferManager.java index 664450b0d..6e413fa31 100644 --- a/source/org/jivesoftware/smackx/filetransfer/FileTransferManager.java +++ b/source/org/jivesoftware/smackx/filetransfer/FileTransferManager.java @@ -27,6 +27,7 @@ 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; @@ -125,18 +126,21 @@ public class FileTransferManager { * Creates an OutgoingFileTransfer to send a file to another user. * * @param userID - * The fully qualified jabber ID with resource of the user to + * 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) { -// Why is this only accepting fully qualified JID? -// if (userID == null || StringUtils.parseName(userID).length() <= 0 -// || StringUtils.parseServer(userID).length() <= 0 -// || StringUtils.parseResource(userID).length() <= 0) { -// throw new IllegalArgumentException( -// "The provided user id was not fully qualified"); -// } + 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(), From 57a5f88ead090d0c6c436c411e3b030d15d15e95 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Fri, 22 Mar 2013 18:14:08 +0000 Subject: [PATCH 13/41] SMACK-384 Don't use a semaphore while waiting for PacketReader to be started. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13577 b35dd754-fafc-0310-a699-88a17e54d16e --- source/org/jivesoftware/smack/PacketReader.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/source/org/jivesoftware/smack/PacketReader.java b/source/org/jivesoftware/smack/PacketReader.java index af1c7682f..826360cf3 100644 --- a/source/org/jivesoftware/smack/PacketReader.java +++ b/source/org/jivesoftware/smack/PacketReader.java @@ -51,7 +51,6 @@ class PacketReader { volatile boolean done; private String connectionID = null; - private Semaphore connectionSemaphore; protected PacketReader(final XMPPConnection connection) { this.connection = connection; @@ -97,21 +96,17 @@ class PacketReader { * @throws XMPPException if the server fails to send an opening stream back * for more than five seconds. */ - public void startup() throws XMPPException { - connectionSemaphore = new Semaphore(1); - + synchronized public void startup() throws XMPPException { readerThread.start(); - // Wait for stream tag before returing. We'll wait a couple of seconds before + // Wait for stream tag before returning. We'll wait a couple of seconds before // giving up and throwing an error. try { - connectionSemaphore.acquire(); - // 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(); - connectionSemaphore.tryAcquire(3 * waitTime, TimeUnit.MILLISECONDS); + wait(3 * waitTime); } catch (InterruptedException ie) { // Ignore. @@ -304,8 +299,8 @@ class PacketReader { * 3) TLS negotiation was successful * */ - private void releaseConnectionIDLock() { - connectionSemaphore.release(); + synchronized private void releaseConnectionIDLock() { + notify(); } /** From ebfe3e69ed8a5217f9cb80e7c2491c29a758cfa3 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Fri, 22 Mar 2013 18:14:15 +0000 Subject: [PATCH 14/41] SMACK-382 Prevent memory leak in AdHocCommandManager by only creating the Thread if it's actually needed git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13578 b35dd754-fafc-0310-a699-88a17e54d16e --- .../smackx/commands/AdHocCommandManager.java | 81 +++++++++---------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java b/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java index f32c48ec2..8f4eb65c5 100755 --- a/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java +++ b/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java @@ -343,46 +343,7 @@ public class AdHocCommandManager { PacketFilter filter = new PacketTypeFilter(AdHocCommandData.class); connection.addPacketListener(listener, filter); - // Create a thread to reap sessions. But, we'll only start it later when commands are - // actually registered. - 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 = null; } /** @@ -486,7 +447,45 @@ public class AdHocCommandManager { response.setStatus(Status.executing); executingCommands.put(sessionId, command); // See if the session reaping thread is started. If not, start it. - if (!sessionsSweeper.isAlive()) { + 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(); } } From 374a12b73a7bf149e2caf92e07209dee249ccc0d Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sat, 23 Mar 2013 00:30:44 +0000 Subject: [PATCH 15/41] Document how an Avatar can be removed from a vCard. Add a convenience method. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13585 b35dd754-fafc-0310-a699-88a17e54d16e --- .../org/jivesoftware/smackx/packet/VCard.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/source/org/jivesoftware/smackx/packet/VCard.java b/source/org/jivesoftware/smackx/packet/VCard.java index 376044d05..db5ef76a9 100644 --- a/source/org/jivesoftware/smackx/packet/VCard.java +++ b/source/org/jivesoftware/smackx/packet/VCard.java @@ -338,22 +338,22 @@ public class VCard extends IQ { } /** - * Specify the bytes for the avatar to use. + * Removes the avatar from the vCard * - * @param bytes the bytes of the avatar. + * This is done by setting the PHOTO value to the empty string as defined in XEP-0153 + */ + public void removeAvatar() { + setAvatar(null, "image/jpeg"); + } + + /** + * Specify the bytes for the avatar to use. + * If bytes is null, then the avatar will be removed. + * + * @param bytes the bytes of the avatar, or null to remove the avatar data */ public void setAvatar(byte[] bytes) { - if (bytes == null) { - // Remove avatar (if any) from mappings - otherUnescapableFields.remove("PHOTO"); - return; - } - - // Otherwise, add to mappings. - String encodedImage = StringUtils.encodeBase64(bytes); - avatar = encodedImage; - - setField("PHOTO", "image/jpeg" + encodedImage + "", true); + setAvatar(bytes, "image/jpeg"); } /** From 07a0b8f7223e965fb797739ca06eba2cdab1f20e Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Sat, 23 Mar 2013 00:30:59 +0000 Subject: [PATCH 16/41] SMACK-413 fixed vCard parsing regarding the PHOTO element. Moved vCard test cases to unit-test where appropriate. Added testcases for vCard PHOTO parsing. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13586 b35dd754-fafc-0310-a699-88a17e54d16e --- .../org/jivesoftware/smackx/packet/VCard.java | 66 +++++++---- .../smackx/provider/VCardProvider.java | 37 +++++- .../jivesoftware/smackx/VCardUnitTest.java | 107 ++++++++++++++++++ .../smack/test/SmackTestCase.java | 2 - test/org/jivesoftware/smackx/VCardTest.java | 37 ++---- 5 files changed, 197 insertions(+), 52 deletions(-) create mode 100644 test-unit/org/jivesoftware/smackx/VCardUnitTest.java diff --git a/source/org/jivesoftware/smackx/packet/VCard.java b/source/org/jivesoftware/smackx/packet/VCard.java index db5ef76a9..9766db824 100644 --- a/source/org/jivesoftware/smackx/packet/VCard.java +++ b/source/org/jivesoftware/smackx/packet/VCard.java @@ -112,7 +112,8 @@ public class VCard extends IQ { private String organization; private String organizationUnit; - private String avatar; + private String photoMimeType; + private String photoBinval; /** * Such as DESC ROLE GEO etc.. see JEP-0054 @@ -343,12 +344,15 @@ public class VCard extends IQ { * This is done by setting the PHOTO value to the empty string as defined in XEP-0153 */ public void removeAvatar() { - setAvatar(null, "image/jpeg"); + // Remove avatar (if any) + photoBinval = null; + photoMimeType = null; } /** - * Specify the bytes for the avatar to use. + * 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 */ @@ -363,27 +367,27 @@ public class VCard extends IQ { * @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) { - // Remove avatar (if any) from mappings - otherUnescapableFields.remove("PHOTO"); + removeAvatar(); return; } // Otherwise, add to mappings. String encodedImage = StringUtils.encodeBase64(bytes); - avatar = encodedImage; - setField("PHOTO", "" + mimeType + "" + encodedImage + "", true); + setAvatar(encodedImage, mimeType); } /** - * Set the encoded avatar string. This is used by the provider. + * Specify the Avatar used for this vCard. * - * @param encodedAvatar the encoded avatar string. + * @param encodedImage the Base64 encoded image as String + * @param mimeType the MIME type of the image */ - public void setEncodedImage(String encodedAvatar) { - //TODO Move VCard and VCardProvider into a vCard package. - this.avatar = encodedAvatar; + public void setAvatar(String encodedImage, String mimeType) { + photoBinval = encodedImage; + photoMimeType = mimeType; } /** @@ -410,10 +414,19 @@ public class VCard extends IQ { * @return byte representation of avatar. */ public byte[] getAvatar() { - if (avatar == null) { + if (photoBinval == null) { return null; } - return StringUtils.decodeBase64(avatar); + 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; } /** @@ -570,16 +583,14 @@ public class VCard extends IQ { return sb.toString(); } - private void copyFieldsFrom(VCard result) { - if (result == null) result = new VCard(); - + 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(result)); + field.set(this, field.get(from)); } catch (IllegalAccessException e) { throw new RuntimeException("This cannot happen:" + field, e); @@ -612,6 +623,7 @@ public class VCard extends IQ { || homePhones.size() > 0 || workAddr.size() > 0 || workPhones.size() > 0 + || photoBinval != null ; } @@ -666,8 +678,11 @@ public class VCard extends IQ { if (!workAddr.equals(vCard.workAddr)) { return false; } - return workPhones.equals(vCard.workPhones); + if (photoBinval != null ? !photoBinval.equals(vCard.photoBinval) : vCard.photoBinval != null) { + return false; + } + return workPhones.equals(vCard.workPhones); } public int hashCode() { @@ -684,6 +699,7 @@ public class VCard extends IQ { 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; } @@ -716,6 +732,7 @@ public class VCard extends IQ { appendOrganization(); appendGenericFields(); + appendPhoto(); appendEmail(emailWork, "WORK"); appendEmail(emailHome, "HOME"); @@ -727,6 +744,17 @@ public class VCard extends IQ { 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() { diff --git a/source/org/jivesoftware/smackx/provider/VCardProvider.java b/source/org/jivesoftware/smackx/provider/VCardProvider.java index 3f07af1fc..8fa04211d 100644 --- a/source/org/jivesoftware/smackx/provider/VCardProvider.java +++ b/source/org/jivesoftware/smackx/provider/VCardProvider.java @@ -114,7 +114,7 @@ public class VCardProvider implements IQProvider { vCard.setFirstName(getTagContents("GIVEN")); vCard.setLastName(getTagContents("FAMILY")); vCard.setMiddleName(getTagContents("MIDDLE")); - vCard.setEncodedImage(getTagContents("BINVAL")); + setupPhoto(); setupEmails(); @@ -127,6 +127,41 @@ public class VCardProvider implements IQProvider { 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 nodes = new ArrayList(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; diff --git a/test-unit/org/jivesoftware/smackx/VCardUnitTest.java b/test-unit/org/jivesoftware/smackx/VCardUnitTest.java new file mode 100644 index 000000000..17adf75a7 --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/VCardUnitTest.java @@ -0,0 +1,107 @@ +package org.jivesoftware.smackx; + +import static org.junit.Assert.*; + +import java.util.Arrays; + +import org.junit.Test; + +import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smackx.packet.VCard; +import org.jivesoftware.smackx.provider.VCardProvider; + +public class VCardUnitTest { + + @Test + public void testNoWorkHomeSpecifier_EMAIL() throws Throwable { + VCard card = VCardProvider.createVCardFromXML("foo@fee.www.bar"); + assertEquals("foo@fee.www.bar", card.getEmailHome()); + } + + @Test + public void testNoWorkHomeSpecifier_TEL() throws Throwable { + VCard card = VCardProvider.createVCardFromXML("3443233"); + assertEquals("3443233", card.getPhoneWork("FAX")); + } + + @Test + public void testNoWorkHomeSpecifier_ADDR() throws Throwable { + VCard card = VCardProvider.createVCardFromXML("Some streetddss"); + assertEquals("Some street", card.getAddressFieldWork("STREET")); + assertEquals("ddss", card.getAddressFieldWork("FF")); + } + + @Test + public void testFN() throws Throwable { + VCard card = VCardProvider.createVCardFromXML("kir max"); + assertEquals("kir max", card.getField("FN")); + // assertEquals("kir max", card.getFullName()); + } + + private final static String MIME_TYPE = "testtype"; + private final static String VCARD_XML = "" + getAvatarEncoded() + "" + MIME_TYPE + ""; + @Test + public void testPhoto() throws Throwable { + VCard vc = VCardProvider.createVCardFromXML(VCARD_XML); + byte[] avatar = vc.getAvatar(); + String mimeType = vc.getAvatarMimeType(); + assertEquals(mimeType, MIME_TYPE); + + byte[] expectedAvatar = getAvatarBinary(); + assertTrue(Arrays.equals(avatar, expectedAvatar)); + } + + public static byte[] getAvatarBinary() { + return StringUtils.decodeBase64(getAvatarEncoded()); + } + private static String getAvatarEncoded() { + return "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"; + } +} diff --git a/test/org/jivesoftware/smack/test/SmackTestCase.java b/test/org/jivesoftware/smack/test/SmackTestCase.java index fa6bb2d20..48e017743 100644 --- a/test/org/jivesoftware/smack/test/SmackTestCase.java +++ b/test/org/jivesoftware/smack/test/SmackTestCase.java @@ -272,8 +272,6 @@ public abstract class SmackTestCase extends TestCase { try { getConnection(i).login(currentUser, currentPassword, "Smack"); } catch (XMPPException e) { - e.printStackTrace(); - // Create the test accounts if (!getConnection(0).getAccountManager().supportsAccountCreation()) fail("Server does not support account creation"); diff --git a/test/org/jivesoftware/smackx/VCardTest.java b/test/org/jivesoftware/smackx/VCardTest.java index 08eb8b5d3..ac32a2494 100644 --- a/test/org/jivesoftware/smackx/VCardTest.java +++ b/test/org/jivesoftware/smackx/VCardTest.java @@ -23,7 +23,6 @@ import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.test.SmackTestCase; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.packet.VCard; -import org.jivesoftware.smackx.provider.VCardProvider; /** * Created by IntelliJ IDEA. @@ -81,36 +80,12 @@ public class VCardTest extends SmackTestCase { //assertEquals("Should load another user's VCard successfully", origVCard.toString(), loaded.toString()); assertEquals("Should load another user's VCard successfully", origVCard, loaded); - - } - - public void testNoWorkHomeSpecifier_EMAIL() throws Throwable { - VCard card = VCardProvider.createVCardFromXML("foo@fee.www.bar"); - assertEquals("foo@fee.www.bar", card.getEmailHome()); - } - - public void testNoWorkHomeSpecifier_TEL() throws Throwable { - VCard card = VCardProvider.createVCardFromXML("3443233"); - assertEquals("3443233", card.getPhoneWork("FAX")); - } - - public void testNoWorkHomeSpecifier_ADDR() throws Throwable { - VCard card = VCardProvider.createVCardFromXML("Some streetddss"); - assertEquals("Some street", card.getAddressFieldWork("STREET")); - assertEquals("ddss", card.getAddressFieldWork("FF")); - } - - public void testFN() throws Throwable { - VCard card = VCardProvider.createVCardFromXML("kir max"); - assertEquals("kir max", card.getField("FN")); - // assertEquals("kir max", card.getFullName()); } public void testBinaryAvatar() throws Throwable { VCard card = new VCard(); card.setAvatar(getAvatarBinary()); card.save(getConnection(0)); - System.out.println(card.getChildElementXML()); VCard loaded = new VCard(); try { @@ -120,8 +95,10 @@ public class VCardTest extends SmackTestCase { e.printStackTrace(); fail(e.getMessage()); } - System.out.println(StringUtils.encodeBase64(loaded.getAvatar())); - assertEquals("Should load own Avatar successfully", card.getAvatar(), loaded.getAvatar()); + + byte[] initialAvatar = card.getAvatar(); + byte[] loadedAvatar = loaded.getAvatar(); + assertEquals("Should load own Avatar successfully", initialAvatar, loadedAvatar); loaded = new VCard(); try { @@ -135,11 +112,11 @@ public class VCardTest extends SmackTestCase { assertEquals("Should load avatar successfully", card.getAvatar(), loaded.getAvatar()); } - private byte[] getAvatarBinary() { - return StringUtils.decodeBase64(getAvatarEnconded()); + public static byte[] getAvatarBinary() { + return StringUtils.decodeBase64(getAvatarEncoded()); } - private String getAvatarEnconded() { + public static String getAvatarEncoded() { return "/9j/4AAQSkZJRgABAQEASABIAAD/4QAWRXhpZgAATU0AKgAAAAgAAAAAAAD/2wBDAAUDBAQEAwUE\n" + "BAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/\n" + "2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e\n" + From 999c86ef4cd74f40bda84d478355c74a46b0ab13 Mon Sep 17 00:00:00 2001 From: rcollier Date: Sat, 23 Mar 2013 11:55:43 +0000 Subject: [PATCH 17/41] SMACK-225 Modified DNSUtils to return a default HostAddress when no DNS resolver is available. This restores the previous default behaviour. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13587 b35dd754-fafc-0310-a699-88a17e54d16e --- source/org/jivesoftware/smack/util/DNSUtil.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/source/org/jivesoftware/smack/util/DNSUtil.java b/source/org/jivesoftware/smack/util/DNSUtil.java index 628d8e8f3..20a143faf 100644 --- a/source/org/jivesoftware/smack/util/DNSUtil.java +++ b/source/org/jivesoftware/smack/util/DNSUtil.java @@ -81,7 +81,12 @@ public class DNSUtil { * @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 resolveXMPPDomain(String domain) { + public static List resolveXMPPDomain(final String domain) { + if (dnsResolver == null) { + List addresses = new ArrayList(1); + addresses.add(new HostAddress(domain, 5222)); + return addresses; + } return resolveDomain(domain, 'c'); } @@ -102,7 +107,12 @@ public class DNSUtil { * @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 resolveXMPPServerDomain(String domain) { + public static List resolveXMPPServerDomain(final String domain) { + if (dnsResolver == null) { + List addresses = new ArrayList(1); + addresses.add(new HostAddress(domain, 5269)); + return addresses; + } return resolveDomain(domain, 's'); } @@ -117,9 +127,6 @@ public class DNSUtil { } } - if (dnsResolver == null) - throw new IllegalStateException("No DNS resolver active."); - List addresses = new ArrayList(); // Step one: Do SRV lookups From a14178990ba9568326c5f5fd8a282e12fe4ea829 Mon Sep 17 00:00:00 2001 From: rcollier Date: Sat, 23 Mar 2013 11:59:08 +0000 Subject: [PATCH 18/41] SMACK-412 Added the pingMyServer back in, cleaned up unneeded synchronization and removed minimum ping interval. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13588 b35dd754-fafc-0310-a699-88a17e54d16e --- .../org/jivesoftware/smack/PacketWriter.java | 18 ++-- source/org/jivesoftware/smack/SmackError.java | 24 ++++++ .../org/jivesoftware/smack/XMPPException.java | 26 +++++- .../smack/ping/ServerPingManager.java | 13 +-- .../smack/util/SyncPacketSend.java | 3 +- .../jivesoftware/smackx/ping/PingManager.java | 21 ++++- ...ServerPingTest.java => KeepaliveTest.java} | 27 ++++-- .../ping/{PingPongTest.java => PingTest.java} | 84 +++++++++++++++---- 8 files changed, 174 insertions(+), 42 deletions(-) create mode 100644 source/org/jivesoftware/smack/SmackError.java rename test-unit/org/jivesoftware/smack/ping/{ServerPingTest.java => KeepaliveTest.java} (87%) rename test-unit/org/jivesoftware/smackx/ping/{PingPongTest.java => PingTest.java} (69%) diff --git a/source/org/jivesoftware/smack/PacketWriter.java b/source/org/jivesoftware/smack/PacketWriter.java index 559e2afd9..1e30439c8 100644 --- a/source/org/jivesoftware/smack/PacketWriter.java +++ b/source/org/jivesoftware/smack/PacketWriter.java @@ -162,12 +162,10 @@ class PacketWriter { while (!done && (writerThread == thisThread)) { Packet packet = nextPacket(); if (packet != null) { - synchronized (writer) { - writer.write(packet.toXML()); + writer.write(packet.toXML()); + writer.flush(); + if (queue.isEmpty()) { writer.flush(); - if (queue.isEmpty()) { - writer.flush(); - } } } } @@ -175,13 +173,11 @@ class PacketWriter { // we won't have time to entirely flush it before the socket is forced closed // by the shutdown process. try { - synchronized (writer) { - while (!queue.isEmpty()) { - Packet packet = queue.remove(); - writer.write(packet.toXML()); - } - writer.flush(); + while (!queue.isEmpty()) { + Packet packet = queue.remove(); + writer.write(packet.toXML()); } + writer.flush(); } catch (Exception e) { e.printStackTrace(); diff --git a/source/org/jivesoftware/smack/SmackError.java b/source/org/jivesoftware/smack/SmackError.java new file mode 100644 index 000000000..af22e7c0e --- /dev/null +++ b/source/org/jivesoftware/smack/SmackError.java @@ -0,0 +1,24 @@ +package org.jivesoftware.smack; + +public enum SmackError { + NO_RESPONSE_FROM_SERVER("No response from server."); + + private String message; + + private SmackError(String errMessage) { + message = errMessage; + } + + public String getErrorMessage() { + return message; + } + + public static SmackError getErrorCode(String message) { + for (SmackError code : values()) { + if (code.message.equals(message)) { + return code; + } + } + return null; + } +} diff --git a/source/org/jivesoftware/smack/XMPPException.java b/source/org/jivesoftware/smack/XMPPException.java index 6da24c2b7..e6da0168c 100644 --- a/source/org/jivesoftware/smack/XMPPException.java +++ b/source/org/jivesoftware/smack/XMPPException.java @@ -41,10 +41,12 @@ import java.io.PrintWriter; * @author Matt Tucker */ public class XMPPException extends Exception { - + private static final long serialVersionUID = 6881651633890968625L; + private StreamError streamError = null; private XMPPError error = null; private Throwable wrappedThrowable = null; + private SmackError smackError = null; /** * Creates a new XMPPException. @@ -62,6 +64,16 @@ public class XMPPException extends Exception { super(message); } + /** + * Creates a new XMPPException with a Smack specific error code. + * + * @param code the root cause of the exception. + */ + public XMPPException(SmackError code) { + super(code.getErrorMessage()); + smackError = code; + } + /** * Creates a new XMPPException with the Throwable that was the root cause of the * exception. @@ -74,7 +86,7 @@ public class XMPPException extends Exception { } /** - * Cretaes a new XMPPException with the stream error that was the root case of the + * Creates 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. * @@ -144,6 +156,16 @@ public class XMPPException extends Exception { return error; } + /** + * Returns the SmackError asscociated with this exception, or null if there + * isn't one. + * + * @return the SmackError asscociated with this exception. + */ + public SmackError getSmackError() { + return smackError; + } + /** * Returns the StreamError asscociated with this exception, or null if there * isn't one. The underlying TCP connection is closed by the server after sending the diff --git a/source/org/jivesoftware/smack/ping/ServerPingManager.java b/source/org/jivesoftware/smack/ping/ServerPingManager.java index 171710bdf..b9525d7f1 100644 --- a/source/org/jivesoftware/smack/ping/ServerPingManager.java +++ b/source/org/jivesoftware/smack/ping/ServerPingManager.java @@ -57,8 +57,6 @@ import org.jivesoftware.smackx.ServiceDiscoveryManager; * @author Florian Schmaus */ public class ServerPingManager { - public static final long PING_MINIMUM = 10000; - private static Map instances = Collections .synchronizedMap(new WeakHashMap()); private static long defaultPingInterval = SmackConfiguration.getKeepAliveInterval(); @@ -173,12 +171,15 @@ public class ServerPingManager { * The new ping time interval in milliseconds. */ public void setPingInterval(long newPingInterval) { - if (newPingInterval < PING_MINIMUM) - newPingInterval = PING_MINIMUM; - if (pingInterval != newPingInterval) { pingInterval = newPingInterval; - schedulePingServerTask(); + + if (pingInterval < 0) { + stopPinging(); + } + else { + schedulePingServerTask(); + } } } diff --git a/source/org/jivesoftware/smack/util/SyncPacketSend.java b/source/org/jivesoftware/smack/util/SyncPacketSend.java index d658637f0..6506cbd88 100644 --- a/source/org/jivesoftware/smack/util/SyncPacketSend.java +++ b/source/org/jivesoftware/smack/util/SyncPacketSend.java @@ -16,6 +16,7 @@ package org.jivesoftware.smack.util; import org.jivesoftware.smack.PacketCollector; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.SmackError; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.filter.PacketFilter; import org.jivesoftware.smack.filter.PacketIDFilter; @@ -47,7 +48,7 @@ final public class SyncPacketSend response.cancel(); if (result == null) { - throw new XMPPException("No response from " + packet.getTo()); + throw new XMPPException(SmackError.NO_RESPONSE_FROM_SERVER); } else if (result.getError() != null) { throw new XMPPException(result.getError()); diff --git a/source/org/jivesoftware/smackx/ping/PingManager.java b/source/org/jivesoftware/smackx/ping/PingManager.java index 7e7e9d2cf..9ab6f026e 100644 --- a/source/org/jivesoftware/smackx/ping/PingManager.java +++ b/source/org/jivesoftware/smackx/ping/PingManager.java @@ -18,6 +18,7 @@ package org.jivesoftware.smackx.ping; import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.SmackError; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.ping.ServerPingManager; import org.jivesoftware.smack.ping.packet.Ping; @@ -49,7 +50,9 @@ public class PingManager { } /** - * Pings the given jid. This method will return false if an error occurs. + * Pings the given jid. This method will return false if an error occurs. The exception + * to this, is a server ping, which will always return true if the server is reachable, + * event if there is an error on the ping itself (i.e. ping not supported). *

* Use {@link #isPingSupported(String)} to determine if XMPP Ping is supported * by the entity. @@ -65,7 +68,8 @@ public class PingManager { SyncPacketSend.getReply(connection, ping); } catch (XMPPException exc) { - return false; + + return (jid.equals(connection.getServiceName()) && (exc.getSmackError() != SmackError.NO_RESPONSE_FROM_SERVER)); } return true; } @@ -92,4 +96,17 @@ public class PingManager { DiscoverInfo result = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid); return result.containsFeature(Ping.NAMESPACE); } + + /** + * Pings the server. This method will return true if the server is reachable. It + * is the equivalent of calling ping with the XMPP domain. + *

+ * Unlike the {@link #ping(String)} case, this method will return true even if + * {@link #isPingSupported(String)} is false. + * + * @return true if a reply was received from the server, false otherwise. + */ + public boolean pingMyServer() { + return ping(connection.getServiceName()); + } } diff --git a/test-unit/org/jivesoftware/smack/ping/ServerPingTest.java b/test-unit/org/jivesoftware/smack/ping/KeepaliveTest.java similarity index 87% rename from test-unit/org/jivesoftware/smack/ping/ServerPingTest.java rename to test-unit/org/jivesoftware/smack/ping/KeepaliveTest.java index fa16dce03..25927d4e8 100644 --- a/test-unit/org/jivesoftware/smack/ping/ServerPingTest.java +++ b/test-unit/org/jivesoftware/smack/ping/KeepaliveTest.java @@ -21,9 +21,12 @@ import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.ping.packet.Ping; import org.jivesoftware.smack.util.PacketParserUtils; +import org.junit.After; +import org.junit.Before; import org.junit.Test; -public class ServerPingTest { +public class KeepaliveTest { + private static final long PING_MINIMUM = 1000; private static String TO = "juliet@capulet.lit/balcony"; private static String ID = "s2c1"; @@ -31,7 +34,21 @@ public class ServerPingTest { { outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); } - + + private int originalTimeout; + + @Before + public void resetProperties() + { + originalTimeout = SmackConfiguration.getPacketReplyTimeout(); + SmackConfiguration.setPacketReplyTimeout(1000); + } + + @After + public void restoreProperties() + { + SmackConfiguration.setPacketReplyTimeout(originalTimeout); + } /* * Stanza copied from spec */ @@ -133,7 +150,7 @@ public class ServerPingTest { private DummyConnection getConnection() { DummyConnection con = new DummyConnection(); ServerPingManager mgr = ServerPingManager.getInstanceFor(con); - mgr.setPingInterval(ServerPingManager.PING_MINIMUM); + mgr.setPingInterval(PING_MINIMUM); return con; } @@ -141,7 +158,7 @@ public class ServerPingTest { private ThreadedDummyConnection getThreadedConnection() { ThreadedDummyConnection con = new ThreadedDummyConnection(); ServerPingManager mgr = ServerPingManager.getInstanceFor(con); - mgr.setPingInterval(ServerPingManager.PING_MINIMUM); + mgr.setPingInterval(PING_MINIMUM); return con; } @@ -157,6 +174,6 @@ public class ServerPingTest { } private long getWaitTime() { - return ServerPingManager.PING_MINIMUM + SmackConfiguration.getPacketReplyTimeout() + 3000; + return PING_MINIMUM + SmackConfiguration.getPacketReplyTimeout() + 3000; } } diff --git a/test-unit/org/jivesoftware/smackx/ping/PingPongTest.java b/test-unit/org/jivesoftware/smackx/ping/PingTest.java similarity index 69% rename from test-unit/org/jivesoftware/smackx/ping/PingPongTest.java rename to test-unit/org/jivesoftware/smackx/ping/PingTest.java index 0346bb034..b7b6358b8 100644 --- a/test-unit/org/jivesoftware/smackx/ping/PingPongTest.java +++ b/test-unit/org/jivesoftware/smackx/ping/PingTest.java @@ -23,19 +23,28 @@ import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.ping.packet.Ping; import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.junit.Before; import org.junit.Test; import static org.junit.Assert.*; -public class PingPongTest { - +public class PingTest { + private DummyConnection dummyCon; + private ThreadedDummyConnection threadedCon; + + @Before + public void setup() { + dummyCon = new DummyConnection(); + threadedCon = new ThreadedDummyConnection(); + } + @Test public void checkSendingPing() throws Exception { - DummyConnection con = new DummyConnection(); - PingManager pinger = new PingManager(con); + dummyCon = new DummyConnection(); + PingManager pinger = new PingManager(dummyCon); pinger.ping("test@myserver.com"); - Packet sentPacket = con.getSentPacket(); + Packet sentPacket = dummyCon.getSentPacket(); assertTrue(sentPacket instanceof Ping); @@ -43,9 +52,9 @@ public class PingPongTest { @Test public void checkSuccessfulPing() throws Exception { - ThreadedDummyConnection con = new ThreadedDummyConnection(); + threadedCon = new ThreadedDummyConnection(); - PingManager pinger = new PingManager(con); + PingManager pinger = new PingManager(threadedCon); boolean pingSuccess = pinger.ping("test@myserver.com"); @@ -59,8 +68,8 @@ public class PingPongTest { */ @Test public void checkFailedPingOnTimeout() throws Exception { - DummyConnection con = new DummyConnection(); - PingManager pinger = new PingManager(con); + dummyCon = new DummyConnection(); + PingManager pinger = new PingManager(dummyCon); boolean pingSuccess = pinger.ping("test@myserver.com"); @@ -69,12 +78,12 @@ public class PingPongTest { } /** - * DummyConnection will not reply so it will timeout. + * Server returns an exception for entity. * @throws Exception */ @Test - public void checkFailedPingError() throws Exception { - ThreadedDummyConnection con = new ThreadedDummyConnection(); + public void checkFailedPingToEntityError() throws Exception { + threadedCon = new ThreadedDummyConnection(); //@formatter:off String reply = "" + @@ -84,17 +93,62 @@ public class PingPongTest { "" + ""; //@formatter:on + IQ serviceUnavailable = PacketParserUtils.parseIQ(TestUtils.getIQParser(reply), threadedCon); + threadedCon.addIQReply(serviceUnavailable); + + PingManager pinger = new PingManager(threadedCon); + + boolean pingSuccess = pinger.ping("test@myserver.com"); + + assertFalse(pingSuccess); + } + + @Test + public void checkPingToServerSuccess() throws Exception { + ThreadedDummyConnection con = new ThreadedDummyConnection(); + PingManager pinger = new PingManager(con); + + boolean pingSuccess = pinger.pingMyServer(); + + assertTrue(pingSuccess); + } + + /** + * Server returns an exception. + * @throws Exception + */ + @Test + public void checkPingToServerError() throws Exception { + ThreadedDummyConnection con = new ThreadedDummyConnection(); + //@formatter:off + String reply = + "" + + "" + + "" + + "" + + "" + + ""; + //@formatter:on IQ serviceUnavailable = PacketParserUtils.parseIQ(TestUtils.getIQParser(reply), con); con.addIQReply(serviceUnavailable); PingManager pinger = new PingManager(con); - boolean pingSuccess = pinger.ping("test@myserver.com"); - - assertFalse(pingSuccess); + boolean pingSuccess = pinger.pingMyServer(); + assertTrue(pingSuccess); } + @Test + public void checkPingToServerTimeout() throws Exception { + DummyConnection con = new DummyConnection(); + PingManager pinger = new PingManager(con); + + boolean pingSuccess = pinger.pingMyServer(); + + assertFalse(pingSuccess); + } + @Test public void checkSuccessfulDiscoRequest() throws Exception { ThreadedDummyConnection con = new ThreadedDummyConnection(); From b0bdd56b2049a5f615f284ae7e3798aec7437c68 Mon Sep 17 00:00:00 2001 From: rcollier Date: Sat, 23 Mar 2013 13:27:55 +0000 Subject: [PATCH 19/41] SMACK-412 Fixed merge error. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13589 b35dd754-fafc-0310-a699-88a17e54d16e --- source/org/jivesoftware/smack/PacketWriter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/org/jivesoftware/smack/PacketWriter.java b/source/org/jivesoftware/smack/PacketWriter.java index 1e30439c8..3fe4db09f 100644 --- a/source/org/jivesoftware/smack/PacketWriter.java +++ b/source/org/jivesoftware/smack/PacketWriter.java @@ -163,7 +163,7 @@ class PacketWriter { Packet packet = nextPacket(); if (packet != null) { writer.write(packet.toXML()); - writer.flush(); + if (queue.isEmpty()) { writer.flush(); } From d1fdd497d9317a74123844f000217c27bd82ee8b Mon Sep 17 00:00:00 2001 From: rcollier Date: Sat, 23 Mar 2013 22:18:38 +0000 Subject: [PATCH 20/41] SMACK-415 Applied patch to fix improper use of parser. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13590 b35dd754-fafc-0310-a699-88a17e54d16e --- .../smackx/pubsub/provider/ItemProvider.java | 118 +++++++++++------- 1 file changed, 75 insertions(+), 43 deletions(-) diff --git a/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java b/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java index aabd4cb2f..833392a43 100644 --- a/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java +++ b/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java @@ -24,53 +24,85 @@ import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace; import org.xmlpull.v1.XmlPullParser; /** - * Parses an item 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 - * smack.providers for its element name and namespace. If no provider is registered, it will return a {@link SimplePayload}. + * Parses an item 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 smack.providers 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 class ItemProvider implements PacketExtensionProvider { - public PacketExtension parseExtension(XmlPullParser parser) throws Exception - { - String id = parser.getAttributeValue(null, "id"); + 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) - { - boolean done = false; - StringBuilder payloadText = new StringBuilder(); - - while (!done) - { - if (tag == XmlPullParser.END_TAG && parser.getName().equals(elem)) - done = true; - else if (!((tag == XmlPullParser.START_TAG) && parser.isEmptyElementTag())) - payloadText.append(parser.getText()); - - if (!done) - tag = parser.next(); - } - return new PayloadItem(id, node, new SimplePayload(payloadElemName, payloadNS, payloadText.toString())); - } - else - { - return new PayloadItem(id, node, PacketParserUtils.parsePacketExtension(payloadElemName, payloadNS, parser)); - } - } - } + 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) + { + boolean done = false; + StringBuilder payloadText = new StringBuilder(); + + 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) && (!payloadNS.isEmpty())) + 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("/>"); + done = true; + } + else + { + payloadText.append(">"); + } + } + else if (parser.getEventType() == XmlPullParser.END_TAG) + { + if (done) + done = false; + else + payloadText.append(""); + } + else if (parser.getEventType() == XmlPullParser.TEXT) + { + payloadText.append(parser.getText()); + } + tag = parser.next(); + } + return new PayloadItem(id, node, new SimplePayload(payloadElemName, payloadNS, payloadText.toString())); + } + else { + return new PayloadItem(id, node, PacketParserUtils.parsePacketExtension( + payloadElemName, payloadNS, parser)); + } + } + } } From 998245be26b3c84cd76d803e4c8cd07448e73af7 Mon Sep 17 00:00:00 2001 From: rcollier Date: Sat, 23 Mar 2013 22:23:19 +0000 Subject: [PATCH 21/41] SMACK-412 Fixed merge error. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13591 b35dd754-fafc-0310-a699-88a17e54d16e --- build/resources/META-INF/smack-config.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/build/resources/META-INF/smack-config.xml b/build/resources/META-INF/smack-config.xml index 7009ab468..52421c703 100644 --- a/build/resources/META-INF/smack-config.xml +++ b/build/resources/META-INF/smack-config.xml @@ -33,6 +33,7 @@ org.jivesoftware.smackx.LastActivityManager org.jivesoftware.smack.ReconnectionManager org.jivesoftware.smackx.commands.AdHocCommandManager + org.jivesoftware.smack.util.dns.JavaxResolver From 58f56ee31b11eb8eaf3a5afd42b61b2b94273926 Mon Sep 17 00:00:00 2001 From: rcollier Date: Sun, 24 Mar 2013 15:02:25 +0000 Subject: [PATCH 22/41] SMACK-413 Put removed public method back in and marked as deprecated. We can remove in major release. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13592 b35dd754-fafc-0310-a699-88a17e54d16e --- source/org/jivesoftware/smackx/packet/VCard.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/source/org/jivesoftware/smackx/packet/VCard.java b/source/org/jivesoftware/smackx/packet/VCard.java index 9766db824..70d2d59fa 100644 --- a/source/org/jivesoftware/smackx/packet/VCard.java +++ b/source/org/jivesoftware/smackx/packet/VCard.java @@ -85,7 +85,8 @@ import org.jivesoftware.smack.util.StringUtils; * @author Kirill Maximov (kir@maxkir.com) */ public class VCard extends IQ { - + private static final String DEFAULT_MIME_TYPE = "image/jpeg"; + /** * Phone types: * VOICE?, FAX?, PAGER?, MSG?, CELL?, VIDEO?, BBS?, MODEM?, ISDN?, PCS?, PREF? @@ -93,7 +94,6 @@ public class VCard extends IQ { private Map homePhones = new HashMap(); private Map workPhones = new HashMap(); - /** * Address types: * POSTAL?, PARCEL?, (DOM | INTL)?, PREF?, POBOX?, EXTADR?, STREET?, LOCALITY?, @@ -357,7 +357,7 @@ public class VCard extends IQ { * @param bytes the bytes of the avatar, or null to remove the avatar data */ public void setAvatar(byte[] bytes) { - setAvatar(bytes, "image/jpeg"); + setAvatar(bytes, DEFAULT_MIME_TYPE); } /** @@ -390,6 +390,16 @@ public class VCard extends IQ { photoMimeType = mimeType; } + /** + * Set the encoded avatar string. This is used by the provider. + * + * @param encodedAvatar the encoded avatar string. + * @deprecated Use {@link #setAvatar(String, String)} instead. + */ + public void setEncodedImage(String encodedAvatar) { + setAvatar(encodedAvatar, DEFAULT_MIME_TYPE); + } + /** * Return the byte representation of the avatar(if one exists), otherwise returns null if * no avatar could be found. From 0fdfd6e75ee4703af17745f4fb9a4738912f1493 Mon Sep 17 00:00:00 2001 From: rcollier Date: Sun, 31 Mar 2013 14:24:50 +0000 Subject: [PATCH 23/41] SMACK-361 Some general code cleanup added some missing hashcode methods and added back some removed public API methods (marked as deprecated). git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13598 b35dd754-fafc-0310-a699-88a17e54d16e --- .../smack/util/Base32Encoder.java | 5 +- .../smack/util/Base64Encoder.java | 5 +- .../smack/util/StringEncoder.java | 6 +- source/org/jivesoftware/smackx/FormField.java | 25 +++++--- .../smackx/NodeInformationProvider.java | 8 +-- .../smackx/ServiceDiscoveryManager.java | 2 +- .../smackx/entitycaps/EntityCapsManager.java | 6 +- .../cache/EntityCapsPersistentCache.java | 6 +- .../provider/CapsExtensionProvider.java | 2 +- .../jivesoftware/smackx/packet/DataForm.java | 2 +- .../smackx/packet/DiscoverInfo.java | 58 +++++++++++++++---- .../smack/util/ConnectionUtils.java | 2 + 12 files changed, 84 insertions(+), 43 deletions(-) diff --git a/source/org/jivesoftware/smack/util/Base32Encoder.java b/source/org/jivesoftware/smack/util/Base32Encoder.java index c7cc1d028..0a4ea21e7 100644 --- a/source/org/jivesoftware/smack/util/Base32Encoder.java +++ b/source/org/jivesoftware/smack/util/Base32Encoder.java @@ -29,7 +29,7 @@ import java.io.IOException; */ public class Base32Encoder implements StringEncoder { - private static Base32Encoder instance; + private static Base32Encoder instance = new Base32Encoder(); private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ2345678"; private Base32Encoder() { @@ -37,9 +37,6 @@ public class Base32Encoder implements StringEncoder { } public static Base32Encoder getInstance() { - if (instance == null) { - instance = new Base32Encoder(); - } return instance; } diff --git a/source/org/jivesoftware/smack/util/Base64Encoder.java b/source/org/jivesoftware/smack/util/Base64Encoder.java index 78399b463..8d29f12c5 100644 --- a/source/org/jivesoftware/smack/util/Base64Encoder.java +++ b/source/org/jivesoftware/smack/util/Base64Encoder.java @@ -20,16 +20,13 @@ package org.jivesoftware.smack.util; */ public class Base64Encoder implements StringEncoder { - private static Base64Encoder instance; + private static Base64Encoder instance = new Base64Encoder(); private Base64Encoder() { // Use getInstance() } public static Base64Encoder getInstance() { - if (instance == null) { - instance = new Base64Encoder(); - } return instance; } diff --git a/source/org/jivesoftware/smack/util/StringEncoder.java b/source/org/jivesoftware/smack/util/StringEncoder.java index 5a15c9548..4c3d373f3 100644 --- a/source/org/jivesoftware/smack/util/StringEncoder.java +++ b/source/org/jivesoftware/smack/util/StringEncoder.java @@ -17,8 +17,6 @@ */ package org.jivesoftware.smack.util; -// TODO move StringEncoder, Base64Encoder and Base32Encoder to smack.util - public interface StringEncoder { /** * Encodes an string to another representation @@ -26,7 +24,7 @@ public interface StringEncoder { * @param string * @return */ - public String encode(String string); + String encode(String string); /** * Decodes an string back to it's initial representation @@ -34,5 +32,5 @@ public interface StringEncoder { * @param string * @return */ - public String decode(String string); + String decode(String string); } diff --git a/source/org/jivesoftware/smackx/FormField.java b/source/org/jivesoftware/smackx/FormField.java index 44dfa8cec..3d15e921a 100644 --- a/source/org/jivesoftware/smackx/FormField.java +++ b/source/org/jivesoftware/smackx/FormField.java @@ -299,24 +299,23 @@ public class FormField { return buf.toString(); } + @Override public boolean equals(Object obj) { if (obj == null) return false; if (obj == this) return true; - if (obj.getClass() != getClass()) + if (!(obj instanceof FormField)) return false; FormField other = (FormField) obj; - String thisXml = toXML(); - String otherXml = other.toXML(); + return toXML().equals(other.toXML()); + } - if (thisXml.equals(otherXml)) { - return true; - } else { - return false; - } + @Override + public int hashCode() { + return toXML().hashCode(); } /** @@ -356,6 +355,7 @@ public class FormField { return value; } + @Override public String toString() { return getLabel(); } @@ -375,6 +375,7 @@ public class FormField { return buf.toString(); } + @Override public boolean equals(Object obj) { if (obj == null) return false; @@ -396,5 +397,13 @@ public class FormField { 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/source/org/jivesoftware/smackx/NodeInformationProvider.java b/source/org/jivesoftware/smackx/NodeInformationProvider.java index 68bb613d3..27ee53a01 100644 --- a/source/org/jivesoftware/smackx/NodeInformationProvider.java +++ b/source/org/jivesoftware/smackx/NodeInformationProvider.java @@ -45,7 +45,7 @@ public interface NodeInformationProvider { * * @return a list of the Items defined in the node. */ - public abstract List getNodeItems(); + List getNodeItems(); /** * Returns a list of the features defined in the node. For @@ -55,7 +55,7 @@ public interface NodeInformationProvider { * * @return a list of the feature strings defined in the node. */ - public abstract List getNodeFeatures(); + List getNodeFeatures(); /** * Returns a list of the indentites defined in the node. For @@ -64,12 +64,12 @@ public interface NodeInformationProvider { * * @return a list of the Identities defined in the node. */ - public abstract List getNodeIdentities(); + List getNodeIdentities(); /** * Returns a list of the packet extensions defined in the node. * * @return a list of the packet extensions defined in the node. */ - public abstract List getNodePacketExtensions(); + List getNodePacketExtensions(); } diff --git a/source/org/jivesoftware/smackx/ServiceDiscoveryManager.java b/source/org/jivesoftware/smackx/ServiceDiscoveryManager.java index f0e7912aa..9e31f6777 100644 --- a/source/org/jivesoftware/smackx/ServiceDiscoveryManager.java +++ b/source/org/jivesoftware/smackx/ServiceDiscoveryManager.java @@ -513,7 +513,7 @@ public class ServiceDiscoveryManager { // If the node version is known, store the new entry. if (nvh != null) { - if (EntityCapsManager.verifyDiscvoerInfoVersion(nvh.getVer(), nvh.getHash(), info)) + if (EntityCapsManager.verifyDiscoverInfoVersion(nvh.getVer(), nvh.getHash(), info)) EntityCapsManager.addDiscoverInfoByNode(nvh.getNodeVer(), info); } diff --git a/source/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java b/source/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java index d5d6402d2..1da222efd 100644 --- a/source/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java +++ b/source/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java @@ -506,7 +506,7 @@ public class EntityCapsManager { * @param info * @return true if it's valid and should be cache, false if not */ - public static boolean verifyDiscvoerInfoVersion(String ver, String hash, DiscoverInfo info) { + public static boolean verifyDiscoverInfoVersion(String ver, String hash, DiscoverInfo info) { // step 3.3 check for duplicate identities if (info.containsDuplicateIdentities()) return false; @@ -583,7 +583,7 @@ public class EntityCapsManager { // NAME is not included (in accordance with XEP-0030, the category and // type MUST be included. SortedSet sortedIdentities = new TreeSet(); - ; + for (Iterator it = discoverInfo.getIdentities(); it.hasNext();) sortedIdentities.add(it.next()); @@ -616,7 +616,7 @@ public class EntityCapsManager { // 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.hasHiddenFromTypeField()) { + 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., diff --git a/source/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java b/source/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java index 4247e7b1b..044104351 100644 --- a/source/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java +++ b/source/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java @@ -24,15 +24,15 @@ public interface EntityCapsPersistentCache { * @param node * @param info */ - abstract void addDiscoverInfoByNodePersistent(String node, DiscoverInfo info); + void addDiscoverInfoByNodePersistent(String node, DiscoverInfo info); /** * Replay the Caches data into EntityCapsManager */ - abstract void replay() throws IOException; + void replay() throws IOException; /** * Empty the Cache */ - abstract void emptyCache(); + void emptyCache(); } diff --git a/source/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java b/source/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java index 4328d21b3..a112cd52b 100644 --- a/source/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java +++ b/source/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java @@ -1,4 +1,4 @@ -/* +/** * Copyright 2009 Jonas Ã…dahl. * Copyright 2011-2013 Florian Schmaus * diff --git a/source/org/jivesoftware/smackx/packet/DataForm.java b/source/org/jivesoftware/smackx/packet/DataForm.java index 4bc1f6994..4d12892b5 100644 --- a/source/org/jivesoftware/smackx/packet/DataForm.java +++ b/source/org/jivesoftware/smackx/packet/DataForm.java @@ -202,7 +202,7 @@ public class DataForm implements PacketExtension { * * @return */ - public boolean hasHiddenFromTypeField() { + public boolean hasHiddenFormTypeField() { boolean found = false; for (FormField f : fields) { if (f.getVariable().equals("FORM_TYPE") && f.getType() != null && f.getType().equals("hidden")) diff --git a/source/org/jivesoftware/smackx/packet/DiscoverInfo.java b/source/org/jivesoftware/smackx/packet/DiscoverInfo.java index e2219032d..ba873a96d 100644 --- a/source/org/jivesoftware/smackx/packet/DiscoverInfo.java +++ b/source/org/jivesoftware/smackx/packet/DiscoverInfo.java @@ -257,13 +257,25 @@ public class DiscoverInfo extends IQ { * attributes. * */ - public static class Identity implements Comparable { + public static class Identity implements Comparable { 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 @@ -274,6 +286,9 @@ public class DiscoverInfo extends IQ { * @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; @@ -313,6 +328,7 @@ public class DiscoverInfo extends IQ { * 'type' attribute refer to Jabber::Registrar * * @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; @@ -374,11 +390,14 @@ public class DiscoverInfo extends IQ { String otherLang = other.lang == null ? "" : other.lang; String thisLang = lang == null ? "" : lang; - - if (!other.type.equals(type)) - return false; 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; @@ -387,23 +406,35 @@ public class DiscoverInfo extends IQ { 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 and identity with another object. The comparison order is: + * 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(Object obj) { - - DiscoverInfo.Identity other = (DiscoverInfo.Identity) obj; + 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 (type.equals(other.type)) { + 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 @@ -412,7 +443,7 @@ public class DiscoverInfo extends IQ { return thisLang.compareTo(otherLang); } } else { - return type.compareTo(other.type); + return thisType.compareTo(otherType); } } else { return category.compareTo(other.category); @@ -436,6 +467,8 @@ public class DiscoverInfo extends IQ { * @param variable the feature's variable. */ public Feature(String variable) { + if (variable == null) + throw new IllegalArgumentException("variable cannot be null"); this.variable = variable; } @@ -465,5 +498,10 @@ public class DiscoverInfo extends IQ { DiscoverInfo.Feature other = (DiscoverInfo.Feature) obj; return variable.equals(other.variable); } + + @Override + public int hashCode() { + return 37 * variable.hashCode(); + } } } diff --git a/test/org/jivesoftware/smack/util/ConnectionUtils.java b/test/org/jivesoftware/smack/util/ConnectionUtils.java index 6d9f49106..74426793e 100644 --- a/test/org/jivesoftware/smack/util/ConnectionUtils.java +++ b/test/org/jivesoftware/smack/util/ConnectionUtils.java @@ -6,6 +6,8 @@ import org.jivesoftware.smack.XMPPException; public class ConnectionUtils { + private ConnectionUtils() {} + public static void becomeFriends(Connection con0, Connection con1) throws XMPPException { Roster r0 = con0.getRoster(); Roster r1 = con1.getRoster(); From 0a1e72bb5f64521d2f7dc6d8ce1482c5743c9060 Mon Sep 17 00:00:00 2001 From: rcollier Date: Sun, 31 Mar 2013 14:26:30 +0000 Subject: [PATCH 24/41] SMACK-413 Removed some commented out code. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13599 b35dd754-fafc-0310-a699-88a17e54d16e --- test/org/jivesoftware/smackx/VCardTest.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/test/org/jivesoftware/smackx/VCardTest.java b/test/org/jivesoftware/smackx/VCardTest.java index ac32a2494..e38e183b2 100644 --- a/test/org/jivesoftware/smackx/VCardTest.java +++ b/test/org/jivesoftware/smackx/VCardTest.java @@ -66,10 +66,8 @@ public class VCardTest extends SmackTestCase { fail(e.getMessage()); } - //assertEquals("Should load own VCard successfully", origVCard.toString(), loaded.toString()); assertEquals("Should load own VCard successfully", origVCard, loaded); - loaded = new VCard(); try { loaded.load(getConnection(1), getBareJID(0)); @@ -78,7 +76,6 @@ public class VCardTest extends SmackTestCase { fail(e.getMessage()); } - //assertEquals("Should load another user's VCard successfully", origVCard.toString(), loaded.toString()); assertEquals("Should load another user's VCard successfully", origVCard, loaded); } @@ -167,20 +164,6 @@ public class VCardTest extends SmackTestCase { "CgADtrKoqPLpKaXPVXUdPtnXTNUBLlTQR4xHlj+gHT/7pjw8oTsf/9k="; } - /* - public void testFullName() throws Throwable { - VCard card = new VCard(); - card.setFirstName("kir"); - // assertEquals("kir", card.getFullName()); - - card.setLastName("maximov"); - // assertEquals("kir maximov", card.getFullName()); - - card.setField("FN", "some name"); - // assertEquals("some name", card.getFullName()); - } - */ - protected int getMaxConnections() { return 2; } From 9da54ecbce2c165cd4c627a519d2519e53218513 Mon Sep 17 00:00:00 2001 From: rcollier Date: Mon, 1 Apr 2013 13:40:02 +0000 Subject: [PATCH 25/41] SMACK-225 Converted abstract class to interface, added missing hashcode method, fixed typos and some minor name changes and added licensing text git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13600 b35dd754-fafc-0310-a699-88a17e54d16e --- build/resources/releasedocs/README.html | 30 +++++++++++++++++++ .../smack/util/dns/DNSJavaResolver.java | 11 +++---- .../smack/util/dns/DNSResolver.java | 13 ++++++-- .../smack/util/dns/HostAddress.java | 24 ++++++++++++--- .../smack/util/dns/JavaxResolver.java | 8 ++--- .../smack/util/dns/SRVRecord.java | 14 +++++---- .../jivesoftware/smack/util/DNSUtilTest.java | 4 +-- 7 files changed, 81 insertions(+), 23 deletions(-) diff --git a/build/resources/releasedocs/README.html b/build/resources/releasedocs/README.html index de92dbc8f..f935622c9 100644 --- a/build/resources/releasedocs/README.html +++ b/build/resources/releasedocs/README.html @@ -203,6 +203,36 @@ last release. to use these icons outside of Smack.
  • Third-party source code is licensed as noted in their source files. +
  • Third-party binary code is licensed as follows. +
    + dnsjava (http://dnsjava.org)
    +
    + Copyright (c) 1999-2005, Brian Wellington
    + All rights reserved.
    +	
    + Redistribution and use in source and binary forms, with or without
    + modification, are permitted provided that the following conditions are met:
    +	
    +     * Redistributions of source code must retain the above copyright notice,
    +       this list of conditions and the following disclaimer.
    +     * Redistributions in binary form must reproduce the above copyright notice,
    +       this list of conditions and the following disclaimer in the documentation
    +       and/or other materials provided with the distribution.
    +     * Neither the name of the dnsjava project nor the names of its contributors
    +       may be used to endorse or promote products derived from this software
    +       without specific prior written permission.
    +	
    + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
    + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
    + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
    + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
    + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    +
  • diff --git a/source/org/jivesoftware/smack/util/dns/DNSJavaResolver.java b/source/org/jivesoftware/smack/util/dns/DNSJavaResolver.java index 91db73b1a..dd93fd3ae 100644 --- a/source/org/jivesoftware/smack/util/dns/DNSJavaResolver.java +++ b/source/org/jivesoftware/smack/util/dns/DNSJavaResolver.java @@ -22,18 +22,19 @@ import org.xbill.DNS.Lookup; import org.xbill.DNS.Record; import org.xbill.DNS.Type; -public class DNSJavaResolver extends DNSResolver { +/** + * This implementation uses the dnsjava implementation for resolving DNS addresses. + * + */ +public class DNSJavaResolver implements DNSResolver { - private static DNSJavaResolver instance; + private static DNSJavaResolver instance = new DNSJavaResolver(); private DNSJavaResolver() { } public static DNSResolver getInstance() { - if (instance == null) { - instance = new DNSJavaResolver(); - } return instance; } diff --git a/source/org/jivesoftware/smack/util/dns/DNSResolver.java b/source/org/jivesoftware/smack/util/dns/DNSResolver.java index 2c5dd296c..86f037b6f 100644 --- a/source/org/jivesoftware/smack/util/dns/DNSResolver.java +++ b/source/org/jivesoftware/smack/util/dns/DNSResolver.java @@ -17,8 +17,17 @@ package org.jivesoftware.smack.util.dns; import java.util.List; -public abstract class DNSResolver { +/** + * Implementations of this interface define a class that is capable of resolving DNS addresses. + * + */ +public interface DNSResolver { - public abstract List lookupSRVRecords(String name); + /** + * 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 lookupSRVRecords(String name); } diff --git a/source/org/jivesoftware/smack/util/dns/HostAddress.java b/source/org/jivesoftware/smack/util/dns/HostAddress.java index 978a6de69..eb8b07ad7 100644 --- a/source/org/jivesoftware/smack/util/dns/HostAddress.java +++ b/source/org/jivesoftware/smack/util/dns/HostAddress.java @@ -23,10 +23,10 @@ public class HostAddress { /** * Creates a new HostAddress with the given FQDN. The port will be set to the default XMPP client port: 5222 * - * @param fqdn - * @throws IllegalArgumentException + * @param fqdn Fully qualified domain name. + * @throws IllegalArgumentException If the fqdn is null. */ - public HostAddress(String fqdn) throws IllegalArgumentException { + public HostAddress(String fqdn) { if (fqdn == null) throw new IllegalArgumentException("FQDN is null"); if (fqdn.charAt(fqdn.length() - 1) == '.') { @@ -39,7 +39,14 @@ public class HostAddress { this.port = 5222; } - public HostAddress(String fqdn, int port) throws IllegalArgumentException { + /** + * 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( @@ -60,10 +67,12 @@ public class HostAddress { this.exception = e; } + @Override public String toString() { return fqdn + ":" + port; } + @Override public boolean equals(Object o) { if (this == o) { return true; @@ -80,6 +89,13 @@ public class HostAddress { 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) { diff --git a/source/org/jivesoftware/smack/util/dns/JavaxResolver.java b/source/org/jivesoftware/smack/util/dns/JavaxResolver.java index 4ea361fdf..ae3dbf6f6 100644 --- a/source/org/jivesoftware/smack/util/dns/JavaxResolver.java +++ b/source/org/jivesoftware/smack/util/dns/JavaxResolver.java @@ -28,12 +28,12 @@ import javax.naming.directory.InitialDirContext; import org.jivesoftware.smack.util.DNSUtil; /** - * A DNS resolver (mostly for SRV records), which makes use of the API provided in the javax.* namepsace. + * A DNS resolver (mostly for SRV records), which makes use of the API provided in the javax.* namespace. * * @author Florian Schmaus * */ -public class JavaxResolver extends DNSResolver { +public class JavaxResolver implements DNSResolver { private static JavaxResolver instance; private static DirContext dirContext; @@ -48,14 +48,14 @@ public class JavaxResolver extends DNSResolver { } // Try to set this DNS resolver as primary one - DNSUtil.setDNSResolver(maybeGetInstance()); + DNSUtil.setDNSResolver(getInstance()); } private JavaxResolver() { } - public static DNSResolver maybeGetInstance() { + public static DNSResolver getInstance() { if (instance == null && isSupported()) { instance = new JavaxResolver(); } diff --git a/source/org/jivesoftware/smack/util/dns/SRVRecord.java b/source/org/jivesoftware/smack/util/dns/SRVRecord.java index 87c6e54fc..457e40eca 100644 --- a/source/org/jivesoftware/smack/util/dns/SRVRecord.java +++ b/source/org/jivesoftware/smack/util/dns/SRVRecord.java @@ -29,13 +29,13 @@ public class SRVRecord extends HostAddress implements Comparable { /** * Create a new SRVRecord * - * @param fqdn - * @param port - * @param priority - * @param weight - * @throws IllegalArgumentException + * @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) throws IllegalArgumentException { + public SRVRecord(String fqdn, int port, int priority, int weight) { super(fqdn, port); if (weight < 0 || weight > 65535) throw new IllegalArgumentException( @@ -60,6 +60,7 @@ public class SRVRecord extends HostAddress implements Comparable { 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". @@ -71,6 +72,7 @@ public class SRVRecord extends HostAddress implements Comparable { return res; } + @Override public String toString() { return super.toString() + " prio:" + priority + ":w:" + weight; } diff --git a/test-unit/org/jivesoftware/smack/util/DNSUtilTest.java b/test-unit/org/jivesoftware/smack/util/DNSUtilTest.java index d107f8f0d..2cc7c96c5 100644 --- a/test-unit/org/jivesoftware/smack/util/DNSUtilTest.java +++ b/test-unit/org/jivesoftware/smack/util/DNSUtilTest.java @@ -24,7 +24,7 @@ public class DNSUtilTest { @Test public void xmppClientDomainJavaXTest() { - DNSResolver resolver = JavaxResolver.maybeGetInstance(); + DNSResolver resolver = JavaxResolver.getInstance(); assertNotNull(resolver); DNSUtil.setDNSResolver(resolver); xmppClientDomainTest(); @@ -32,7 +32,7 @@ public class DNSUtilTest { @Test public void xmppServerDomainJavaXTest() { - DNSResolver resolver = JavaxResolver.maybeGetInstance(); + DNSResolver resolver = JavaxResolver.getInstance(); assertNotNull(resolver); DNSUtil.setDNSResolver(resolver); xmppServerDomainTest(); From b28037bc79fd8f88fdf73360c20607c2a9a7e297 Mon Sep 17 00:00:00 2001 From: rcollier Date: Tue, 2 Apr 2013 00:23:55 +0000 Subject: [PATCH 26/41] SMACK-331 Changed methods to existing Smack conventions and moved interface to top level. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13601 b35dd754-fafc-0310-a699-88a17e54d16e --- .../smackx/receipts/DeliveryReceipt.java | 5 +++- .../receipts/DeliveryReceiptManager.java | 20 +++----------- .../receipts/ReceiptReceivedListener.java | 26 +++++++++++++++++++ .../smackx/receipts/DeliveryReceiptTest.java | 14 +++++----- 4 files changed, 39 insertions(+), 26 deletions(-) create mode 100644 source/org/jivesoftware/smackx/receipts/ReceiptReceivedListener.java diff --git a/source/org/jivesoftware/smackx/receipts/DeliveryReceipt.java b/source/org/jivesoftware/smackx/receipts/DeliveryReceipt.java index 14f4456d2..902055671 100644 --- a/source/org/jivesoftware/smackx/receipts/DeliveryReceipt.java +++ b/source/org/jivesoftware/smackx/receipts/DeliveryReceipt.java @@ -1,4 +1,4 @@ -/* +/** * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -42,16 +42,19 @@ public class DeliveryReceipt implements PacketExtension return id; } + @Override public String getElementName() { return ELEMENT; } + @Override public String getNamespace() { return NAMESPACE; } + @Override public String toXML() { return ""; diff --git a/source/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java b/source/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java index eaac5a4c4..125b87e63 100644 --- a/source/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java +++ b/source/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java @@ -24,20 +24,16 @@ 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.PacketExtensionFilter; -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-0184: Message Delivery Receipts. This class implements + * Manager for XEP-0184: Message Delivery Receipts. This class implements * the manager for {@link DeliveryReceipt} support, enabling and disabling of * automatic DeliveryReceipt transmission. * @@ -167,7 +163,7 @@ public class DeliveryReceiptManager implements PacketListener { * * @param listener the listener to be informed about new receipts */ - public void registerReceiptReceivedListener(ReceiptReceivedListener listener) { + public void addReceiptReceivedListener(ReceiptReceivedListener listener) { receiptReceivedListeners.add(listener); } @@ -176,20 +172,10 @@ public class DeliveryReceiptManager implements PacketListener { * * @param listener the listener to be removed */ - public void unregisterReceiptReceivedListener(ReceiptReceivedListener listener) { + public void removeReceiptReceivedListener(ReceiptReceivedListener listener) { receiptReceivedListeners.remove(listener); } - /** - * Interface for received receipt notifications. - * - * Implement this and add a listener to get notified. - */ - public static interface ReceiptReceivedListener { - void onReceiptReceived(String fromJid, String toJid, String receiptId); - } - - /** * Test if a packet requires a delivery receipt. * diff --git a/source/org/jivesoftware/smackx/receipts/ReceiptReceivedListener.java b/source/org/jivesoftware/smackx/receipts/ReceiptReceivedListener.java new file mode 100644 index 000000000..318311371 --- /dev/null +++ b/source/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/test-unit/org/jivesoftware/smackx/receipts/DeliveryReceiptTest.java b/test-unit/org/jivesoftware/smackx/receipts/DeliveryReceiptTest.java index d6e51d740..6b0425a60 100644 --- a/test-unit/org/jivesoftware/smackx/receipts/DeliveryReceiptTest.java +++ b/test-unit/org/jivesoftware/smackx/receipts/DeliveryReceiptTest.java @@ -13,16 +13,14 @@ */ package org.jivesoftware.smackx.receipts; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import java.io.IOException; import java.io.StringReader; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; import java.util.Properties; -import java.util.TimeZone; import org.jivesoftware.smack.DummyConnection; import org.jivesoftware.smack.packet.Message; @@ -76,7 +74,7 @@ public class DeliveryReceiptTest { DeliveryReceiptManager drm = DeliveryReceiptManager.getInstanceFor(c); TestReceiptReceivedListener rrl = new TestReceiptReceivedListener(); - drm.registerReceiptReceivedListener(rrl); + drm.addReceiptReceivedListener(rrl); Message m = new Message("romeo@montague.com", Message.Type.normal); m.setFrom("julia@capulet.com"); @@ -88,7 +86,7 @@ public class DeliveryReceiptTest { assertEquals("original-test-id", rrl.receiptId); } - private static class TestReceiptReceivedListener implements DeliveryReceiptManager.ReceiptReceivedListener { + private static class TestReceiptReceivedListener implements ReceiptReceivedListener { public String receiptId = null; @Override public void onReceiptReceived(String fromJid, String toJid, String receiptId) { From 4155e9ca809d9be35aabe632b41be0e82a2e8ee3 Mon Sep 17 00:00:00 2001 From: rcollier Date: Sun, 7 Apr 2013 13:17:26 +0000 Subject: [PATCH 27/41] SMACK-391 Reformat some new files to Smack standard git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13602 b35dd754-fafc-0310-a699-88a17e54d16e --- .../smack/util/DateFormatType.java | 91 +++++++-------- .../org/jivesoftware/smack/TestUtils.java | 109 ++++++++---------- 2 files changed, 96 insertions(+), 104 deletions(-) diff --git a/source/org/jivesoftware/smack/util/DateFormatType.java b/source/org/jivesoftware/smack/util/DateFormatType.java index a9d563da9..8e0ae8a36 100644 --- a/source/org/jivesoftware/smack/util/DateFormatType.java +++ b/source/org/jivesoftware/smack/util/DateFormatType.java @@ -17,48 +17,49 @@ * 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 -{ - 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"); - - 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()); - } -} +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/test-unit/org/jivesoftware/smack/TestUtils.java b/test-unit/org/jivesoftware/smack/TestUtils.java index f9dfcd6a8..0eae51e35 100644 --- a/test-unit/org/jivesoftware/smack/TestUtils.java +++ b/test-unit/org/jivesoftware/smack/TestUtils.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,61 +17,52 @@ * 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.StringReader; - -import org.xmlpull.mxp1.MXParser; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -final public class TestUtils -{ - private TestUtils() {} - - public static XmlPullParser getIQParser(String stanza) - { - return getParser(stanza, "iq"); - } - - public static XmlPullParser getMessageParser(String stanza) - { - return getParser(stanza, "message"); - } - - public static XmlPullParser getPresenceParser(String stanza) - { - return getParser(stanza, "presence"); - } - - public static XmlPullParser getParser(String stanza, String startTag) - { - XmlPullParser parser = new MXParser(); - try - { - parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - parser.setInput(new StringReader(stanza)); - boolean found = false; - - while (!found) - { - if ((parser.next() == XmlPullParser.START_TAG) && parser.getName().equals(startTag)) - found = true; - } - - if (!found) - throw new IllegalArgumentException("Cannot parse start tag [" + startTag + "] from stanze [" + stanza + "]"); - } - catch (XmlPullParserException e) - { - throw new RuntimeException(e); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - return parser; - } - -} +package org.jivesoftware.smack; + +import java.io.IOException; +import java.io.StringReader; + +import org.xmlpull.mxp1.MXParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +final public class TestUtils { + private TestUtils() { + } + + public static XmlPullParser getIQParser(String stanza) { + return getParser(stanza, "iq"); + } + + public static XmlPullParser getMessageParser(String stanza) { + return getParser(stanza, "message"); + } + + public static XmlPullParser getPresenceParser(String stanza) { + return getParser(stanza, "presence"); + } + + public static XmlPullParser getParser(String stanza, String startTag) { + XmlPullParser parser = new MXParser(); + try { + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new StringReader(stanza)); + boolean found = false; + + while (!found) { + if ((parser.next() == XmlPullParser.START_TAG) && parser.getName().equals(startTag)) + found = true; + } + + if (!found) + throw new IllegalArgumentException("Cannot parse start tag [" + startTag + "] from stanza [" + stanza + + "]"); + } catch (XmlPullParserException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + return parser; + } + +} From ba81a45b8c60e19b56a22408af6e3aded4cd890f Mon Sep 17 00:00:00 2001 From: rcollier Date: Sun, 7 Apr 2013 15:41:20 +0000 Subject: [PATCH 28/41] SMACK-404 Removed unnecessary string construction. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13603 b35dd754-fafc-0310-a699-88a17e54d16e --- .../org/jivesoftware/smack/proxy/HTTPProxySocketFactory.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/source/org/jivesoftware/smack/proxy/HTTPProxySocketFactory.java b/source/org/jivesoftware/smack/proxy/HTTPProxySocketFactory.java index ba1cb1f69..4ee5dd622 100644 --- a/source/org/jivesoftware/smack/proxy/HTTPProxySocketFactory.java +++ b/source/org/jivesoftware/smack/proxy/HTTPProxySocketFactory.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -92,8 +90,7 @@ class HTTPProxySocketFactory else { String password = proxy.getProxyPassword(); - proxyLine = "\r\nProxy-Authorization: Basic " - + new String(StringUtils.encodeBase64(username + ":" + password)); + 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")); From b4432d76273bbbb8bc4c3d4cf1f824905bb1e6d7 Mon Sep 17 00:00:00 2001 From: rcollier Date: Sun, 7 Apr 2013 19:15:32 +0000 Subject: [PATCH 29/41] SMACK-352 Changed or removed copyright notices that should not have been attributed to Jive Software. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13604 b35dd754-fafc-0310-a699-88a17e54d16e --- .../eclipse/settings/org.eclipse.jdt.ui.prefs | 2 +- .../smackx/jingle/ContentNegotiator.java | 2 - .../smackx/jingle/JingleActionEnum.java | 2 - .../smackx/jingle/JingleException.java | 2 - .../smackx/jingle/JingleNegotiatorState.java | 2 - .../smackx/jingle/JingleSession.java | 2 - .../smackx/jingle/JingleSessionState.java | 2 - .../jingle/JingleSessionStateActive.java | 2 - .../jingle/JingleSessionStateEnded.java | 2 - .../jingle/JingleSessionStatePending.java | 2 - .../jingle/JingleSessionStateUnknown.java | 2 - .../smackx/jingle/mediaimpl/JMFInit.java | 2 - .../mediaimpl/sshare/api/DefaultDecoder.java | 2 - .../mediaimpl/sshare/api/DefaultEncoder.java | 2 - .../mediaimpl/sshare/api/ImageDecoder.java | 2 - .../mediaimpl/sshare/api/ImageEncoder.java | 2 - .../mediaimpl/sshare/api/ImageReceiver.java | 2 - .../sshare/api/ImageTransmitter.java | 2 - .../smackx/jingle/nat/HttpServer.java | 2 - .../smackx/jingle/nat/ICECandidate.java | 2 - .../smackx/jingle/nat/TcpUdpBridgeClient.java | 2 - .../smackx/jingle/nat/TcpUdpBridgeServer.java | 2 - .../smackx/jingle/nat/TestResult.java | 2 - .../smackx/jingle/nat/TransportCandidate.java | 2 - .../smackx/jingle/nat/TransportResolver.java | 2 - .../smackx/jingle/JingleManagerTest.java | 2 - .../smackx/jingle/JingleSessionTest.java | 2 - .../smackx/jingle/JingleSupportTests.java | 2 - .../smackx/jingle/PayloadTypeTest.java | 2 - .../smackx/jingle/nat/BasicResolverTest.java | 2 - .../jingle/nat/BridgedResolverTest.java | 2 - .../smackx/jingle/nat/LocalhostTest.java | 2 - .../smackx/jingle/nat/STUNResolverTest.java | 2 - .../jingle/nat/TransportCandidateTest.java | 2 - .../jingle/nat/TransportResolverTest.java | 2 - .../smackx/provider/JingleProviderTest.java | 2 - .../org/jivesoftware/smack/PrivacyList.java | 2 - .../smack/ReconnectionManager.java | 2 - .../smack/debugger/ConsoleDebugger.java | 2 - .../smack/filter/FromContainsFilter.java | 2 +- .../smack/packet/PrivacyItem.java | 2 - .../smack/provider/PrivacyProvider.java | 2 - .../smack/proxy/DirectSocketFactory.java | 2 - .../smack/proxy/ProxyException.java | 2 - .../jivesoftware/smack/proxy/ProxyInfo.java | 2 - .../smack/proxy/Socks4ProxySocketFactory.java | 2 - .../smack/proxy/Socks5ProxySocketFactory.java | 2 - .../smack/util/DateFormatType.java | 2 +- .../smackx/SharedGroupManager.java | 2 +- .../socks5/Socks5BytestreamSession.java | 2 - .../smackx/commands/LocalCommandFactory.java | 2 +- .../smackx/packet/SharedGroupsInfo.java | 2 +- .../smackx/pubsub/CollectionNode.java | 5 +- .../org/jivesoftware/smackx/pubsub/Node.java | 5 +- .../jivesoftware/smackx/pubsub/NodeEvent.java | 5 +- .../jivesoftware/smack/RosterOfflineTest.java | 2 - .../smack/ThreadedDummyConnection.java | 7 +- .../smack/filters/FromMatchesFilterTest.java | 9 +- .../bytestreams/ibb/IBBPacketUtils.java | 2 - .../smackx/bytestreams/ibb/IBBTestsSuite.java | 2 - .../ibb/InBandBytestreamRequestTest.java | 2 - .../InBandBytestreamSessionMessageTest.java | 2 - .../ibb/InBandBytestreamSessionTest.java | 2 - .../FileTransferNegotiatorTest.java | 82 ++++++++------- .../jivesoftware/smackx/muc/RoomInfoTest.java | 99 +++++++++---------- .../smackx/pubsub/ConfigureFormTest.java | 9 +- .../smackx/pubsub/ItemValidationTest.java | 9 +- test/org/jivesoftware/smack/ChatTest.java | 2 +- .../jivesoftware/smack/PacketReaderTest.java | 2 +- .../org/jivesoftware/smack/PrivacyClient.java | 2 - .../jivesoftware/smack/ReconnectionTest.java | 2 - .../RosterInitializedBeforeConnectTest.java | 2 - .../smack/RosterListenerTest.java | 2 - .../jivesoftware/smack/RosterSmackTest.java | 2 +- .../smack/filter/AndFilterTest.java | 2 +- .../smack/filter/FromContainsFilterTest.java | 2 +- .../smack/filter/NotFilterTest.java | 2 +- .../smack/filter/OrFilterTest.java | 2 +- .../smack/filter/PacketIDFilterTest.java | 2 +- .../smack/filter/PacketTypeFilterTest.java | 2 +- .../smack/filter/ToContainsFilterTest.java | 2 +- .../jivesoftware/smack/packet/MockPacket.java | 2 +- .../smack/packet/PrivacyProviderTest.java | 2 - .../smack/packet/PrivacyTest.java | 2 - .../jivesoftware/smack/util/CacheTest.java | 2 +- .../smack/util/XMPPErrorTest.java | 2 +- .../jivesoftware/smackx/FileTransferTest.java | 2 +- test/org/jivesoftware/smackx/FormTest.java | 2 +- .../smackx/packet/RosterExchangeTest.java | 4 - .../smackx/pubsub/CarExtension.java | 12 ++- .../smackx/pubsub/CarExtensionProvider.java | 12 ++- .../smackx/pubsub/EntityUseCases.java | 12 ++- .../pubsub/MultiUserSubscriptionUseCases.java | 9 +- .../smackx/pubsub/OwnerUseCases.java | 12 ++- .../smackx/pubsub/PublisherUseCases.java | 12 ++- .../smackx/pubsub/SubscriberUseCases.java | 12 ++- .../jivesoftware/smackx/pubsub/TestAPI.java | 12 ++- .../smackx/pubsub/TestEvents.java | 12 ++- .../smackx/pubsub/TestMessageContent.java | 12 ++- .../smackx/pubsub/test/PubSubTestCase.java | 12 ++- .../pubsub/test/SingleUserTestCase.java | 12 ++- 101 files changed, 222 insertions(+), 311 deletions(-) diff --git a/build/eclipse/settings/org.eclipse.jdt.ui.prefs b/build/eclipse/settings/org.eclipse.jdt.ui.prefs index 5da3ae153..fcdbc532c 100644 --- a/build/eclipse/settings/org.eclipse.jdt.ui.prefs +++ b/build/eclipse/settings/org.eclipse.jdt.ui.prefs @@ -4,7 +4,7 @@ editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true formatter_profile=_ignite formatter_settings_version=12 org.eclipse.jdt.ui.javadoc=false -org.eclipse.jdt.ui.text.custom_code_templates= +org.eclipse.jdt.ui.text.custom_code_templates= sp_cleanup.add_default_serial_version_id=true sp_cleanup.add_generated_serial_version_id=false sp_cleanup.add_missing_annotations=true diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/ContentNegotiator.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/ContentNegotiator.java index 72431d5f2..9b68c0095 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/ContentNegotiator.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/ContentNegotiator.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleActionEnum.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleActionEnum.java index d0ca2b73c..99d13c147 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleActionEnum.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleActionEnum.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleException.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleException.java index 965369bc8..db53911a1 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleException.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleException.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleNegotiatorState.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleNegotiatorState.java index caa803419..e00ceb7f2 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleNegotiatorState.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleNegotiatorState.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSession.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSession.java index d1f7bd27d..9b53c9b55 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSession.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSession.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionState.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionState.java index 830ec5c7c..ec12c861e 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionState.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionState.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStateActive.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStateActive.java index f698f4a93..e2244a3e7 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStateActive.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStateActive.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStateEnded.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStateEnded.java index a3cefe059..c0b4db229 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStateEnded.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStateEnded.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStatePending.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStatePending.java index 3221da679..a0bf0f255 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStatePending.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStatePending.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStateUnknown.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStateUnknown.java index 17fa61d1b..80c2e4b6c 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStateUnknown.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/JingleSessionStateUnknown.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/JMFInit.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/JMFInit.java index e4fdd73ff..d67ce3b79 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/JMFInit.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/JMFInit.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/DefaultDecoder.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/DefaultDecoder.java index 6fdb17f48..74262eef5 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/DefaultDecoder.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/DefaultDecoder.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/DefaultEncoder.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/DefaultEncoder.java index cd77ee888..435695a5c 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/DefaultEncoder.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/DefaultEncoder.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageDecoder.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageDecoder.java index 2716f90d8..0e4e702c3 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageDecoder.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageDecoder.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageEncoder.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageEncoder.java index 8d01d210c..fb0ba47e2 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageEncoder.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageEncoder.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageReceiver.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageReceiver.java index 169c7ff3e..64a46c4f4 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageReceiver.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageReceiver.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageTransmitter.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageTransmitter.java index f346f757a..31ef5330a 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageTransmitter.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/mediaimpl/sshare/api/ImageTransmitter.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/HttpServer.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/HttpServer.java index 5b056b481..4c55893a9 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/HttpServer.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/HttpServer.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/ICECandidate.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/ICECandidate.java index cf793da07..c9d30a09e 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/ICECandidate.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/ICECandidate.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TcpUdpBridgeClient.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TcpUdpBridgeClient.java index 4a92a091e..ab6055012 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TcpUdpBridgeClient.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TcpUdpBridgeClient.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TcpUdpBridgeServer.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TcpUdpBridgeServer.java index 5a127afd3..61f10e979 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TcpUdpBridgeServer.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TcpUdpBridgeServer.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TestResult.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TestResult.java index 04bf118e3..c0040505d 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TestResult.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TestResult.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportCandidate.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportCandidate.java index 843344b3d..bce1737b2 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportCandidate.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportCandidate.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportResolver.java b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportResolver.java index 67f32107b..db9f19830 100644 --- a/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportResolver.java +++ b/jingle/extension/source/org/jivesoftware/smackx/jingle/nat/TransportResolver.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleManagerTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleManagerTest.java index c3d18d707..9a7ac2354 100644 --- a/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleManagerTest.java +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleManagerTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSessionTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSessionTest.java index 0cb7f6ef0..77b40def0 100644 --- a/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSessionTest.java +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSessionTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSupportTests.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSupportTests.java index c5ef51746..71f2b6071 100644 --- a/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSupportTests.java +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/JingleSupportTests.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/PayloadTypeTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/PayloadTypeTest.java index a4576bf93..95c44ef29 100644 --- a/jingle/extension/test/org/jivesoftware/smackx/jingle/PayloadTypeTest.java +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/PayloadTypeTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BasicResolverTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BasicResolverTest.java index 7cac5d964..bb1b33819 100644 --- a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BasicResolverTest.java +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BasicResolverTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BridgedResolverTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BridgedResolverTest.java index 5ba2e0a91..6a034b5f7 100644 --- a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BridgedResolverTest.java +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/BridgedResolverTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/LocalhostTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/LocalhostTest.java index 89640dece..81c271da4 100644 --- a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/LocalhostTest.java +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/LocalhostTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/STUNResolverTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/STUNResolverTest.java index dcf735bf1..bcd81d503 100644 --- a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/STUNResolverTest.java +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/STUNResolverTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportCandidateTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportCandidateTest.java index ee353e735..e1170f969 100644 --- a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportCandidateTest.java +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportCandidateTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportResolverTest.java b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportResolverTest.java index aefc42a4b..1ae0f5c1f 100644 --- a/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportResolverTest.java +++ b/jingle/extension/test/org/jivesoftware/smackx/jingle/nat/TransportResolverTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/jingle/extension/test/org/jivesoftware/smackx/provider/JingleProviderTest.java b/jingle/extension/test/org/jivesoftware/smackx/provider/JingleProviderTest.java index a4172b758..b02de02cb 100644 --- a/jingle/extension/test/org/jivesoftware/smackx/provider/JingleProviderTest.java +++ b/jingle/extension/test/org/jivesoftware/smackx/provider/JingleProviderTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/source/org/jivesoftware/smack/PrivacyList.java b/source/org/jivesoftware/smack/PrivacyList.java index abb99b742..67d731db7 100644 --- a/source/org/jivesoftware/smack/PrivacyList.java +++ b/source/org/jivesoftware/smack/PrivacyList.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/source/org/jivesoftware/smack/ReconnectionManager.java b/source/org/jivesoftware/smack/ReconnectionManager.java index 46274520d..cc3e3af19 100644 --- a/source/org/jivesoftware/smack/ReconnectionManager.java +++ b/source/org/jivesoftware/smack/ReconnectionManager.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/source/org/jivesoftware/smack/debugger/ConsoleDebugger.java b/source/org/jivesoftware/smack/debugger/ConsoleDebugger.java index 6ca5d81e8..7e078b407 100644 --- a/source/org/jivesoftware/smack/debugger/ConsoleDebugger.java +++ b/source/org/jivesoftware/smack/debugger/ConsoleDebugger.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/source/org/jivesoftware/smack/filter/FromContainsFilter.java b/source/org/jivesoftware/smack/filter/FromContainsFilter.java index bc372f927..f8e9e977b 100644 --- a/source/org/jivesoftware/smack/filter/FromContainsFilter.java +++ b/source/org/jivesoftware/smack/filter/FromContainsFilter.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/source/org/jivesoftware/smack/packet/PrivacyItem.java b/source/org/jivesoftware/smack/packet/PrivacyItem.java index de7f8fe22..2e144eec6 100644 --- a/source/org/jivesoftware/smack/packet/PrivacyItem.java +++ b/source/org/jivesoftware/smack/packet/PrivacyItem.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/source/org/jivesoftware/smack/provider/PrivacyProvider.java b/source/org/jivesoftware/smack/provider/PrivacyProvider.java index 0e0c78a16..62b31205d 100644 --- a/source/org/jivesoftware/smack/provider/PrivacyProvider.java +++ b/source/org/jivesoftware/smack/provider/PrivacyProvider.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/source/org/jivesoftware/smack/proxy/DirectSocketFactory.java b/source/org/jivesoftware/smack/proxy/DirectSocketFactory.java index ef3da408f..3c5bb3999 100644 --- a/source/org/jivesoftware/smack/proxy/DirectSocketFactory.java +++ b/source/org/jivesoftware/smack/proxy/DirectSocketFactory.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/source/org/jivesoftware/smack/proxy/ProxyException.java b/source/org/jivesoftware/smack/proxy/ProxyException.java index 80d8d52ae..b37910c0c 100644 --- a/source/org/jivesoftware/smack/proxy/ProxyException.java +++ b/source/org/jivesoftware/smack/proxy/ProxyException.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/source/org/jivesoftware/smack/proxy/ProxyInfo.java b/source/org/jivesoftware/smack/proxy/ProxyInfo.java index 4a00d73bb..5a7d3541e 100644 --- a/source/org/jivesoftware/smack/proxy/ProxyInfo.java +++ b/source/org/jivesoftware/smack/proxy/ProxyInfo.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/source/org/jivesoftware/smack/proxy/Socks4ProxySocketFactory.java b/source/org/jivesoftware/smack/proxy/Socks4ProxySocketFactory.java index e0cc7c878..6a32c11e9 100644 --- a/source/org/jivesoftware/smack/proxy/Socks4ProxySocketFactory.java +++ b/source/org/jivesoftware/smack/proxy/Socks4ProxySocketFactory.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/source/org/jivesoftware/smack/proxy/Socks5ProxySocketFactory.java b/source/org/jivesoftware/smack/proxy/Socks5ProxySocketFactory.java index 6746ad1a2..23ef62328 100644 --- a/source/org/jivesoftware/smack/proxy/Socks5ProxySocketFactory.java +++ b/source/org/jivesoftware/smack/proxy/Socks5ProxySocketFactory.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/source/org/jivesoftware/smack/util/DateFormatType.java b/source/org/jivesoftware/smack/util/DateFormatType.java index 8e0ae8a36..9253038d7 100644 --- a/source/org/jivesoftware/smack/util/DateFormatType.java +++ b/source/org/jivesoftware/smack/util/DateFormatType.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/source/org/jivesoftware/smackx/SharedGroupManager.java b/source/org/jivesoftware/smackx/SharedGroupManager.java index 8bc39f156..76cd527c5 100644 --- a/source/org/jivesoftware/smackx/SharedGroupManager.java +++ b/source/org/jivesoftware/smackx/SharedGroupManager.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java index 47cf92abb..41ab142db 100644 --- a/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java +++ b/source/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/source/org/jivesoftware/smackx/commands/LocalCommandFactory.java b/source/org/jivesoftware/smackx/commands/LocalCommandFactory.java index 8ada9662d..83fc4553a 100644 --- a/source/org/jivesoftware/smackx/commands/LocalCommandFactory.java +++ b/source/org/jivesoftware/smackx/commands/LocalCommandFactory.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/source/org/jivesoftware/smackx/packet/SharedGroupsInfo.java b/source/org/jivesoftware/smackx/packet/SharedGroupsInfo.java index 341811106..59bd98e7c 100644 --- a/source/org/jivesoftware/smackx/packet/SharedGroupsInfo.java +++ b/source/org/jivesoftware/smackx/packet/SharedGroupsInfo.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/source/org/jivesoftware/smackx/pubsub/CollectionNode.java b/source/org/jivesoftware/smackx/pubsub/CollectionNode.java index 21645afae..dcd1cc4a4 100644 --- a/source/org/jivesoftware/smackx/pubsub/CollectionNode.java +++ b/source/org/jivesoftware/smackx/pubsub/CollectionNode.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,9 +17,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-07-13 - */ package org.jivesoftware.smackx.pubsub; import org.jivesoftware.smack.Connection; diff --git a/source/org/jivesoftware/smackx/pubsub/Node.java b/source/org/jivesoftware/smackx/pubsub/Node.java index a31e778b7..1b0ff5afd 100644 --- a/source/org/jivesoftware/smackx/pubsub/Node.java +++ b/source/org/jivesoftware/smackx/pubsub/Node.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,9 +17,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-07-09 - */ package org.jivesoftware.smackx.pubsub; import java.util.ArrayList; diff --git a/source/org/jivesoftware/smackx/pubsub/NodeEvent.java b/source/org/jivesoftware/smackx/pubsub/NodeEvent.java index 917d3c494..1392e851e 100644 --- a/source/org/jivesoftware/smackx/pubsub/NodeEvent.java +++ b/source/org/jivesoftware/smackx/pubsub/NodeEvent.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,9 +17,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-05-12 - */ package org.jivesoftware.smackx.pubsub; abstract public class NodeEvent diff --git a/test-unit/org/jivesoftware/smack/RosterOfflineTest.java b/test-unit/org/jivesoftware/smack/RosterOfflineTest.java index ff56d8679..3e7f052aa 100644 --- a/test-unit/org/jivesoftware/smack/RosterOfflineTest.java +++ b/test-unit/org/jivesoftware/smack/RosterOfflineTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/test-unit/org/jivesoftware/smack/ThreadedDummyConnection.java b/test-unit/org/jivesoftware/smack/ThreadedDummyConnection.java index daf5fc14c..d293dfee2 100644 --- a/test-unit/org/jivesoftware/smack/ThreadedDummyConnection.java +++ b/test-unit/org/jivesoftware/smack/ThreadedDummyConnection.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -28,6 +26,11 @@ import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.IQ.Type; +/** + * + * @author Robin Collier + * + */ public class ThreadedDummyConnection extends DummyConnection { private BlockingQueue replyQ = new ArrayBlockingQueue(1); private BlockingQueue messageQ = new LinkedBlockingQueue(5); diff --git a/test-unit/org/jivesoftware/smack/filters/FromMatchesFilterTest.java b/test-unit/org/jivesoftware/smack/filters/FromMatchesFilterTest.java index 2e3b57556..56bb93192 100644 --- a/test-unit/org/jivesoftware/smack/filters/FromMatchesFilterTest.java +++ b/test-unit/org/jivesoftware/smack/filters/FromMatchesFilterTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * Copyright 2011 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. @@ -25,7 +25,12 @@ import static org.junit.Assert.assertFalse; import org.jivesoftware.smack.filter.FromMatchesFilter; import org.jivesoftware.smack.packet.Packet; import org.junit.Test; - + +/** + * + * @author Robin Collier + * + */ public class FromMatchesFilterTest { private static final String BASE_JID1 = "ss@muc.myserver.com"; private static final String FULL_JID1_R1 = BASE_JID1 + "/resource"; diff --git a/test-unit/org/jivesoftware/smackx/bytestreams/ibb/IBBPacketUtils.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/IBBPacketUtils.java index 295f1f5fb..bec303762 100644 --- a/test-unit/org/jivesoftware/smackx/bytestreams/ibb/IBBPacketUtils.java +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/IBBPacketUtils.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/test-unit/org/jivesoftware/smackx/bytestreams/ibb/IBBTestsSuite.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/IBBTestsSuite.java index f2d11bb14..158459860 100644 --- a/test-unit/org/jivesoftware/smackx/bytestreams/ibb/IBBTestsSuite.java +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/IBBTestsSuite.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamRequestTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamRequestTest.java index 0e1bf792b..92644fcf5 100644 --- a/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamRequestTest.java +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamRequestTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSessionMessageTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSessionMessageTest.java index c43bc1a00..5789bbb25 100644 --- a/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSessionMessageTest.java +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSessionMessageTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSessionTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSessionTest.java index e7fb76a6b..b715af10a 100644 --- a/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSessionTest.java +++ b/test-unit/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSessionTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/test-unit/org/jivesoftware/smackx/filetransfer/FileTransferNegotiatorTest.java b/test-unit/org/jivesoftware/smackx/filetransfer/FileTransferNegotiatorTest.java index 3ae1b4e04..f9255241e 100644 --- a/test-unit/org/jivesoftware/smackx/filetransfer/FileTransferNegotiatorTest.java +++ b/test-unit/org/jivesoftware/smackx/filetransfer/FileTransferNegotiatorTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * Copyright 2011 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. @@ -17,44 +17,42 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smackx.filetransfer; - -import static org.junit.Assert.*; - -import org.jivesoftware.smack.DummyConnection; -import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.packet.Packet; -import org.jivesoftware.smackx.ServiceDiscoveryManager; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -public class FileTransferNegotiatorTest { - private DummyConnection connection; - - @Before - public void setUp() throws Exception { - // Uncomment this to enable debug output - //Connection.DEBUG_ENABLED = true; - - connection = new DummyConnection(); - connection.connect(); - connection.login("me", "secret"); - new ServiceDiscoveryManager(connection); - } - - @After - public void tearDown() throws Exception { - if (connection != null) - connection.disconnect(); - } - - @Test - public void verifyForm() throws Exception - { - FileTransferNegotiator fileNeg = FileTransferNegotiator.getInstanceFor(connection); - fileNeg.negotiateOutgoingTransfer("me", "streamid", "file", 1024, null, 10); - Packet packet = connection.getSentPacket(); - assertTrue(packet.toXML().indexOf("\"stream-method\" type=\"list-single\"") != -1); - } -} +package org.jivesoftware.smackx.filetransfer; + +import static org.junit.Assert.assertTrue; + +import org.jivesoftware.smack.DummyConnection; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class FileTransferNegotiatorTest { + private DummyConnection connection; + + @Before + public void setUp() throws Exception { + // Uncomment this to enable debug output + // Connection.DEBUG_ENABLED = true; + + connection = new DummyConnection(); + connection.connect(); + connection.login("me", "secret"); + new ServiceDiscoveryManager(connection); + } + + @After + public void tearDown() throws Exception { + if (connection != null) + connection.disconnect(); + } + + @Test + public void verifyForm() throws Exception { + FileTransferNegotiator fileNeg = FileTransferNegotiator.getInstanceFor(connection); + fileNeg.negotiateOutgoingTransfer("me", "streamid", "file", 1024, null, 10); + Packet packet = connection.getSentPacket(); + assertTrue(packet.toXML().indexOf("\"stream-method\" type=\"list-single\"") != -1); + } +} diff --git a/test-unit/org/jivesoftware/smackx/muc/RoomInfoTest.java b/test-unit/org/jivesoftware/smackx/muc/RoomInfoTest.java index 93ab1eeae..77144869d 100644 --- a/test-unit/org/jivesoftware/smackx/muc/RoomInfoTest.java +++ b/test-unit/org/jivesoftware/smackx/muc/RoomInfoTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * Copyright 2011 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. @@ -17,53 +17,50 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.jivesoftware.smackx.muc; - -import org.jivesoftware.smackx.FormField; -import org.jivesoftware.smackx.muc.RoomInfo; -import org.jivesoftware.smackx.packet.DataForm; -import org.jivesoftware.smackx.packet.DiscoverInfo; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertEquals; -import org.junit.Test; - -public class RoomInfoTest -{ - @Test - public void validateRoomWithEmptyForm() - { - DataForm dataForm = new DataForm("result"); - - DiscoverInfo discoInfo = new DiscoverInfo(); - discoInfo.addExtension(dataForm); - RoomInfo roomInfo = new RoomInfo(discoInfo); - assertTrue(roomInfo.getDescription().isEmpty()); - assertTrue(roomInfo.getSubject().isEmpty()); - assertEquals(-1, roomInfo.getOccupantsCount()); - } - - @Test - public void validateRoomWithForm() - { - DataForm dataForm = new DataForm("result"); - - FormField desc = new FormField("muc#roominfo_description"); - desc.addValue("The place for all good witches!"); - dataForm.addField(desc); - - FormField subject = new FormField("muc#roominfo_subject"); - subject.addValue("Spells"); - dataForm.addField(subject); - - FormField occupants = new FormField("muc#roominfo_occupants"); - occupants.addValue("3"); - dataForm.addField(occupants); - - DiscoverInfo discoInfo = new DiscoverInfo(); - discoInfo.addExtension(dataForm); - RoomInfo roomInfo = new RoomInfo(discoInfo); - assertEquals("The place for all good witches!", roomInfo.getDescription()); - assertEquals("Spells", roomInfo.getSubject()); - assertEquals(3, roomInfo.getOccupantsCount()); - } -} +package org.jivesoftware.smackx.muc; + +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.muc.RoomInfo; +import org.jivesoftware.smackx.packet.DataForm; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +public class RoomInfoTest { + @Test + public void validateRoomWithEmptyForm() { + DataForm dataForm = new DataForm("result"); + + DiscoverInfo discoInfo = new DiscoverInfo(); + discoInfo.addExtension(dataForm); + RoomInfo roomInfo = new RoomInfo(discoInfo); + assertTrue(roomInfo.getDescription().isEmpty()); + assertTrue(roomInfo.getSubject().isEmpty()); + assertEquals(-1, roomInfo.getOccupantsCount()); + } + + @Test + public void validateRoomWithForm() { + DataForm dataForm = new DataForm("result"); + + FormField desc = new FormField("muc#roominfo_description"); + desc.addValue("The place for all good witches!"); + dataForm.addField(desc); + + FormField subject = new FormField("muc#roominfo_subject"); + subject.addValue("Spells"); + dataForm.addField(subject); + + FormField occupants = new FormField("muc#roominfo_occupants"); + occupants.addValue("3"); + dataForm.addField(occupants); + + DiscoverInfo discoInfo = new DiscoverInfo(); + discoInfo.addExtension(dataForm); + RoomInfo roomInfo = new RoomInfo(discoInfo); + assertEquals("The place for all good witches!", roomInfo.getDescription()); + assertEquals("Spells", roomInfo.getSubject()); + assertEquals(3, roomInfo.getOccupantsCount()); + } +} diff --git a/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java b/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java index d3167bed4..784c56f39 100644 --- a/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java +++ b/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * Copyright 2011 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. @@ -33,7 +33,12 @@ import org.jivesoftware.smackx.packet.DiscoverInfo.Identity; import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.junit.Assert; import org.junit.Test; - + +/** + * + * @author Robin Collier + * + */ public class ConfigureFormTest { @Test diff --git a/test-unit/org/jivesoftware/smackx/pubsub/ItemValidationTest.java b/test-unit/org/jivesoftware/smackx/pubsub/ItemValidationTest.java index 9b3d234b1..aee47a946 100644 --- a/test-unit/org/jivesoftware/smackx/pubsub/ItemValidationTest.java +++ b/test-unit/org/jivesoftware/smackx/pubsub/ItemValidationTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * Copyright 2011 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. @@ -33,7 +33,12 @@ import org.junit.Before; import org.junit.Test; import org.xmlpull.mxp1.MXParser; import org.xmlpull.v1.XmlPullParser; - + +/** + * + * @author Robin Collier + * + */ public class ItemValidationTest { private ThreadedDummyConnection connection; diff --git a/test/org/jivesoftware/smack/ChatTest.java b/test/org/jivesoftware/smack/ChatTest.java index 10de48a48..1d973dd1a 100644 --- a/test/org/jivesoftware/smack/ChatTest.java +++ b/test/org/jivesoftware/smack/ChatTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * Copyright 2004 Jive Software. * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/org/jivesoftware/smack/PacketReaderTest.java b/test/org/jivesoftware/smack/PacketReaderTest.java index c3fc35010..5ac670be0 100644 --- a/test/org/jivesoftware/smack/PacketReaderTest.java +++ b/test/org/jivesoftware/smack/PacketReaderTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * Copyright 2004 Jive Software. * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/org/jivesoftware/smack/PrivacyClient.java b/test/org/jivesoftware/smack/PrivacyClient.java index 86772d8ea..3dc6cae56 100644 --- a/test/org/jivesoftware/smack/PrivacyClient.java +++ b/test/org/jivesoftware/smack/PrivacyClient.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/test/org/jivesoftware/smack/ReconnectionTest.java b/test/org/jivesoftware/smack/ReconnectionTest.java index 35c085813..25b5b7c87 100644 --- a/test/org/jivesoftware/smack/ReconnectionTest.java +++ b/test/org/jivesoftware/smack/ReconnectionTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/test/org/jivesoftware/smack/RosterInitializedBeforeConnectTest.java b/test/org/jivesoftware/smack/RosterInitializedBeforeConnectTest.java index f031f3031..050e55a2a 100644 --- a/test/org/jivesoftware/smack/RosterInitializedBeforeConnectTest.java +++ b/test/org/jivesoftware/smack/RosterInitializedBeforeConnectTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/test/org/jivesoftware/smack/RosterListenerTest.java b/test/org/jivesoftware/smack/RosterListenerTest.java index d0b19d0ed..335a1545d 100644 --- a/test/org/jivesoftware/smack/RosterListenerTest.java +++ b/test/org/jivesoftware/smack/RosterListenerTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/test/org/jivesoftware/smack/RosterSmackTest.java b/test/org/jivesoftware/smack/RosterSmackTest.java index 8bee8124f..4808c50f7 100644 --- a/test/org/jivesoftware/smack/RosterSmackTest.java +++ b/test/org/jivesoftware/smack/RosterSmackTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/test/org/jivesoftware/smack/filter/AndFilterTest.java b/test/org/jivesoftware/smack/filter/AndFilterTest.java index c06096845..bde061e4c 100644 --- a/test/org/jivesoftware/smack/filter/AndFilterTest.java +++ b/test/org/jivesoftware/smack/filter/AndFilterTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/test/org/jivesoftware/smack/filter/FromContainsFilterTest.java b/test/org/jivesoftware/smack/filter/FromContainsFilterTest.java index c7ea5ebc6..71d3386ca 100644 --- a/test/org/jivesoftware/smack/filter/FromContainsFilterTest.java +++ b/test/org/jivesoftware/smack/filter/FromContainsFilterTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/test/org/jivesoftware/smack/filter/NotFilterTest.java b/test/org/jivesoftware/smack/filter/NotFilterTest.java index 34f00a7f2..135e27457 100644 --- a/test/org/jivesoftware/smack/filter/NotFilterTest.java +++ b/test/org/jivesoftware/smack/filter/NotFilterTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/test/org/jivesoftware/smack/filter/OrFilterTest.java b/test/org/jivesoftware/smack/filter/OrFilterTest.java index 716dfc381..7bbae8bc3 100644 --- a/test/org/jivesoftware/smack/filter/OrFilterTest.java +++ b/test/org/jivesoftware/smack/filter/OrFilterTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/test/org/jivesoftware/smack/filter/PacketIDFilterTest.java b/test/org/jivesoftware/smack/filter/PacketIDFilterTest.java index 9008667d9..febd03897 100644 --- a/test/org/jivesoftware/smack/filter/PacketIDFilterTest.java +++ b/test/org/jivesoftware/smack/filter/PacketIDFilterTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/test/org/jivesoftware/smack/filter/PacketTypeFilterTest.java b/test/org/jivesoftware/smack/filter/PacketTypeFilterTest.java index b567b6e19..832b2afec 100644 --- a/test/org/jivesoftware/smack/filter/PacketTypeFilterTest.java +++ b/test/org/jivesoftware/smack/filter/PacketTypeFilterTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/test/org/jivesoftware/smack/filter/ToContainsFilterTest.java b/test/org/jivesoftware/smack/filter/ToContainsFilterTest.java index 26bd3dae9..38438b03e 100644 --- a/test/org/jivesoftware/smack/filter/ToContainsFilterTest.java +++ b/test/org/jivesoftware/smack/filter/ToContainsFilterTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/test/org/jivesoftware/smack/packet/MockPacket.java b/test/org/jivesoftware/smack/packet/MockPacket.java index e1ae1a1d8..cea2f9060 100644 --- a/test/org/jivesoftware/smack/packet/MockPacket.java +++ b/test/org/jivesoftware/smack/packet/MockPacket.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/test/org/jivesoftware/smack/packet/PrivacyProviderTest.java b/test/org/jivesoftware/smack/packet/PrivacyProviderTest.java index 06f474c9a..f010c336b 100644 --- a/test/org/jivesoftware/smack/packet/PrivacyProviderTest.java +++ b/test/org/jivesoftware/smack/packet/PrivacyProviderTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/test/org/jivesoftware/smack/packet/PrivacyTest.java b/test/org/jivesoftware/smack/packet/PrivacyTest.java index 5ee8dd49f..cead0dc4b 100644 --- a/test/org/jivesoftware/smack/packet/PrivacyTest.java +++ b/test/org/jivesoftware/smack/packet/PrivacyTest.java @@ -3,8 +3,6 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. - * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at diff --git a/test/org/jivesoftware/smack/util/CacheTest.java b/test/org/jivesoftware/smack/util/CacheTest.java index 371b678d2..ff8eb0233 100644 --- a/test/org/jivesoftware/smack/util/CacheTest.java +++ b/test/org/jivesoftware/smack/util/CacheTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. diff --git a/test/org/jivesoftware/smack/util/XMPPErrorTest.java b/test/org/jivesoftware/smack/util/XMPPErrorTest.java index a91d577e0..d7936ad76 100644 --- a/test/org/jivesoftware/smack/util/XMPPErrorTest.java +++ b/test/org/jivesoftware/smack/util/XMPPErrorTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * Copyright 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. diff --git a/test/org/jivesoftware/smackx/FileTransferTest.java b/test/org/jivesoftware/smackx/FileTransferTest.java index 3d9bab3c3..13aac53ee 100644 --- a/test/org/jivesoftware/smackx/FileTransferTest.java +++ b/test/org/jivesoftware/smackx/FileTransferTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * Copyright 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. diff --git a/test/org/jivesoftware/smackx/FormTest.java b/test/org/jivesoftware/smackx/FormTest.java index f63743e92..b189cbaf2 100644 --- a/test/org/jivesoftware/smackx/FormTest.java +++ b/test/org/jivesoftware/smackx/FormTest.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * Copyright 2004 Jive Software. * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/test/org/jivesoftware/smackx/packet/RosterExchangeTest.java b/test/org/jivesoftware/smackx/packet/RosterExchangeTest.java index 91b3d0366..dbd0e55ba 100644 --- a/test/org/jivesoftware/smackx/packet/RosterExchangeTest.java +++ b/test/org/jivesoftware/smackx/packet/RosterExchangeTest.java @@ -17,10 +17,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 01/08/2003 - * - */ package org.jivesoftware.smackx.packet; import java.util.Iterator; diff --git a/test/org/jivesoftware/smackx/pubsub/CarExtension.java b/test/org/jivesoftware/smackx/pubsub/CarExtension.java index 6fe8ea8b3..3af9848c2 100644 --- a/test/org/jivesoftware/smackx/pubsub/CarExtension.java +++ b/test/org/jivesoftware/smackx/pubsub/CarExtension.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,13 +17,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-05-05 - */ package org.jivesoftware.smackx.pubsub; import org.jivesoftware.smack.packet.PacketExtension; - + +/** + * + * @author Robin Collier + * + */ class CarExtension implements PacketExtension { private String color; diff --git a/test/org/jivesoftware/smackx/pubsub/CarExtensionProvider.java b/test/org/jivesoftware/smackx/pubsub/CarExtensionProvider.java index 555aeea5d..fe3095920 100644 --- a/test/org/jivesoftware/smackx/pubsub/CarExtensionProvider.java +++ b/test/org/jivesoftware/smackx/pubsub/CarExtensionProvider.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,15 +17,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-05-05 - */ package org.jivesoftware.smackx.pubsub; import org.jivesoftware.smack.packet.PacketExtension; import org.jivesoftware.smack.provider.PacketExtensionProvider; import org.xmlpull.v1.XmlPullParser; - + +/** + * + * @author Robin Collier + * + */ public class CarExtensionProvider implements PacketExtensionProvider { diff --git a/test/org/jivesoftware/smackx/pubsub/EntityUseCases.java b/test/org/jivesoftware/smackx/pubsub/EntityUseCases.java index 1d7633c3e..12c251c4b 100644 --- a/test/org/jivesoftware/smackx/pubsub/EntityUseCases.java +++ b/test/org/jivesoftware/smackx/pubsub/EntityUseCases.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,9 +17,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-04-09 - */ package org.jivesoftware.smackx.pubsub; import java.util.Iterator; @@ -29,7 +26,12 @@ import org.jivesoftware.smackx.packet.DiscoverInfo; import org.jivesoftware.smackx.packet.DiscoverItems; import org.jivesoftware.smackx.packet.DiscoverInfo.Identity; import org.jivesoftware.smackx.pubsub.test.SingleUserTestCase; - + +/** + * + * @author Robin Collier + * + */ public class EntityUseCases extends SingleUserTestCase { public void testDiscoverPubsubInfo() throws Exception diff --git a/test/org/jivesoftware/smackx/pubsub/MultiUserSubscriptionUseCases.java b/test/org/jivesoftware/smackx/pubsub/MultiUserSubscriptionUseCases.java index 47cc37c1a..031472ed5 100644 --- a/test/org/jivesoftware/smackx/pubsub/MultiUserSubscriptionUseCases.java +++ b/test/org/jivesoftware/smackx/pubsub/MultiUserSubscriptionUseCases.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -25,7 +25,12 @@ import java.util.List; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smackx.pubsub.test.PubSubTestCase; - + +/** + * + * @author Robin Collier + * + */ public class MultiUserSubscriptionUseCases extends PubSubTestCase { diff --git a/test/org/jivesoftware/smackx/pubsub/OwnerUseCases.java b/test/org/jivesoftware/smackx/pubsub/OwnerUseCases.java index 98c2a8692..23e9529f9 100644 --- a/test/org/jivesoftware/smackx/pubsub/OwnerUseCases.java +++ b/test/org/jivesoftware/smackx/pubsub/OwnerUseCases.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,16 +17,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-04-09 - */ package org.jivesoftware.smackx.pubsub; import java.util.Collection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smackx.pubsub.test.SingleUserTestCase; - + +/** + * + * @author Robin Collier + * + */ public class OwnerUseCases extends SingleUserTestCase { public void testCreateInstantNode() throws Exception diff --git a/test/org/jivesoftware/smackx/pubsub/PublisherUseCases.java b/test/org/jivesoftware/smackx/pubsub/PublisherUseCases.java index d54e0c381..4e69c1327 100644 --- a/test/org/jivesoftware/smackx/pubsub/PublisherUseCases.java +++ b/test/org/jivesoftware/smackx/pubsub/PublisherUseCases.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,9 +17,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-04-09 - */ package org.jivesoftware.smackx.pubsub; import java.util.Collection; @@ -30,7 +27,12 @@ import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.packet.XMPPError.Condition; import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace; import org.jivesoftware.smackx.pubsub.test.SingleUserTestCase; - + +/** + * + * @author Robin Collier + * + */ public class PublisherUseCases extends SingleUserTestCase { public void testSendNodeTrNot() throws Exception diff --git a/test/org/jivesoftware/smackx/pubsub/SubscriberUseCases.java b/test/org/jivesoftware/smackx/pubsub/SubscriberUseCases.java index 9a39ce91b..bb1cf3a70 100644 --- a/test/org/jivesoftware/smackx/pubsub/SubscriberUseCases.java +++ b/test/org/jivesoftware/smackx/pubsub/SubscriberUseCases.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,9 +17,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-04-09 - */ package org.jivesoftware.smackx.pubsub; import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; @@ -34,7 +31,12 @@ import java.util.Map; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smackx.FormField; import org.jivesoftware.smackx.pubsub.test.SingleUserTestCase; - + +/** + * + * @author Robin Collier + * + */ public class SubscriberUseCases extends SingleUserTestCase { public void testSubscribe() throws Exception diff --git a/test/org/jivesoftware/smackx/pubsub/TestAPI.java b/test/org/jivesoftware/smackx/pubsub/TestAPI.java index e11cb8f3a..d6ca512dc 100644 --- a/test/org/jivesoftware/smackx/pubsub/TestAPI.java +++ b/test/org/jivesoftware/smackx/pubsub/TestAPI.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,14 +17,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-04-09 - */ package org.jivesoftware.smackx.pubsub; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smackx.pubsub.test.SingleUserTestCase; - + +/** + * + * @author Robin Collier + * + */ public class TestAPI extends SingleUserTestCase { public void testGetNonexistentNode() diff --git a/test/org/jivesoftware/smackx/pubsub/TestEvents.java b/test/org/jivesoftware/smackx/pubsub/TestEvents.java index 53ea3a2fe..9e07a93cc 100644 --- a/test/org/jivesoftware/smackx/pubsub/TestEvents.java +++ b/test/org/jivesoftware/smackx/pubsub/TestEvents.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,9 +17,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-04-22 - */ package org.jivesoftware.smackx.pubsub; import java.util.ArrayList; @@ -36,7 +33,12 @@ import org.jivesoftware.smack.test.SmackTestCase; import org.jivesoftware.smackx.pubsub.listener.ItemDeleteListener; import org.jivesoftware.smackx.pubsub.listener.ItemEventListener; import org.jivesoftware.smackx.pubsub.listener.NodeConfigListener; - + +/** + * + * @author Robin Collier + * + */ public class TestEvents extends SmackTestCase { diff --git a/test/org/jivesoftware/smackx/pubsub/TestMessageContent.java b/test/org/jivesoftware/smackx/pubsub/TestMessageContent.java index af0d4094b..a1ce54285 100644 --- a/test/org/jivesoftware/smackx/pubsub/TestMessageContent.java +++ b/test/org/jivesoftware/smackx/pubsub/TestMessageContent.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,13 +17,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-08-05 - */ package org.jivesoftware.smackx.pubsub; import junit.framework.TestCase; - + +/** + * + * @author Robin Collier + * + */ public class TestMessageContent extends TestCase { String payloadXmlWithNS = ""; diff --git a/test/org/jivesoftware/smackx/pubsub/test/PubSubTestCase.java b/test/org/jivesoftware/smackx/pubsub/test/PubSubTestCase.java index 6da93c79d..f4b599fc7 100644 --- a/test/org/jivesoftware/smackx/pubsub/test/PubSubTestCase.java +++ b/test/org/jivesoftware/smackx/pubsub/test/PubSubTestCase.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,9 +17,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-05-05 - */ package org.jivesoftware.smackx.pubsub.test; import org.jivesoftware.smack.XMPPException; @@ -29,7 +26,12 @@ import org.jivesoftware.smackx.pubsub.ConfigureForm; import org.jivesoftware.smackx.pubsub.FormType; import org.jivesoftware.smackx.pubsub.LeafNode; import org.jivesoftware.smackx.pubsub.PubSubManager; - + +/** + * + * @author Robin Collier + * + */ abstract public class PubSubTestCase extends SmackTestCase { private PubSubManager[] manager; diff --git a/test/org/jivesoftware/smackx/pubsub/test/SingleUserTestCase.java b/test/org/jivesoftware/smackx/pubsub/test/SingleUserTestCase.java index 8a017807e..38d5f73f8 100644 --- a/test/org/jivesoftware/smackx/pubsub/test/SingleUserTestCase.java +++ b/test/org/jivesoftware/smackx/pubsub/test/SingleUserTestCase.java @@ -3,7 +3,7 @@ * $Revision$ * $Date$ * - * Copyright 2003-2007 Jive Software. + * 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. @@ -17,15 +17,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* - * Created on 2009-05-05 - */ package org.jivesoftware.smackx.pubsub.test; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smackx.pubsub.LeafNode; import org.jivesoftware.smackx.pubsub.PubSubManager; - + +/** + * + * @author Robin Collier + * + */ public class SingleUserTestCase extends PubSubTestCase { protected PubSubManager getManager() From 3a4b05ac00806fa819fe25e7244a1ba7ea1d6017 Mon Sep 17 00:00:00 2001 From: rcollier Date: Tue, 16 Apr 2013 01:39:17 +0000 Subject: [PATCH 30/41] SMACK-412 Abstracted the keepalive implementation and set the thread to start and stop on demand. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13610 b35dd754-fafc-0310-a699-88a17e54d16e --- build/resources/META-INF/smack-config.xml | 4 +- .../KeepAliveManager.java} | 158 ++++++++++-------- .../jivesoftware/smackx/ping/PingManager.java | 67 +++++++- .../jivesoftware/smack/DummyConnection.java | 12 ++ .../{ping => keepalive}/KeepaliveTest.java | 36 +--- .../jivesoftware/smackx/ping/PingTest.java | 45 ++++- 6 files changed, 213 insertions(+), 109 deletions(-) rename source/org/jivesoftware/smack/{ping/ServerPingManager.java => keepalive/KeepAliveManager.java} (71%) rename test-unit/org/jivesoftware/smack/{ping => keepalive}/KeepaliveTest.java (79%) diff --git a/build/resources/META-INF/smack-config.xml b/build/resources/META-INF/smack-config.xml index 52421c703..a4df4764c 100644 --- a/build/resources/META-INF/smack-config.xml +++ b/build/resources/META-INF/smack-config.xml @@ -24,7 +24,7 @@ org.jivesoftware.smackx.ServiceDiscoveryManager org.jivesoftware.smack.PrivacyListManager - org.jivesoftware.smack.ping.ServerPingManager + org.jivesoftware.smack.keepalive.KeepAliveManager org.jivesoftware.smackx.XHTMLManager org.jivesoftware.smackx.muc.MultiUserChat org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager @@ -34,7 +34,7 @@ org.jivesoftware.smack.ReconnectionManager org.jivesoftware.smackx.commands.AdHocCommandManager org.jivesoftware.smack.util.dns.JavaxResolver + org.jivesoftware.smackx.ping.PingManager - diff --git a/source/org/jivesoftware/smack/ping/ServerPingManager.java b/source/org/jivesoftware/smack/keepalive/KeepAliveManager.java similarity index 71% rename from source/org/jivesoftware/smack/ping/ServerPingManager.java rename to source/org/jivesoftware/smack/keepalive/KeepAliveManager.java index b9525d7f1..99e1acfba 100644 --- a/source/org/jivesoftware/smack/ping/ServerPingManager.java +++ b/source/org/jivesoftware/smack/keepalive/KeepAliveManager.java @@ -14,13 +14,14 @@ * limitations under the License. */ -package org.jivesoftware.smack.ping; +package org.jivesoftware.smack.keepalive; import java.util.Collections; +import java.util.Date; +import java.util.HashMap; 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; @@ -33,17 +34,11 @@ 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.AndFilter; -import org.jivesoftware.smack.filter.IQTypeFilter; 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.IQ.Type; import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.ping.PingFailedListener; import org.jivesoftware.smack.ping.packet.Ping; -import org.jivesoftware.smackx.ServiceDiscoveryManager; /** * Using an implementation of XMPP Ping (XEP-0199). This @@ -56,25 +51,15 @@ import org.jivesoftware.smackx.ServiceDiscoveryManager; * * @author Florian Schmaus */ -public class ServerPingManager { - private static Map instances = Collections - .synchronizedMap(new WeakHashMap()); - private static long defaultPingInterval = SmackConfiguration.getKeepAliveInterval(); +public class KeepAliveManager { + private static Map instances = new HashMap(); + private static volatile ScheduledExecutorService periodicPingExecutorService; - private static ScheduledExecutorService periodicPingExecutorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() { - @Override - public Thread newThread(Runnable runnable) { - Thread pingThread = new Thread(runnable, "Smack Server Ping"); - pingThread.setDaemon(true); - return pingThread; - } - }); - static { - if (defaultPingInterval > 0) { + if (SmackConfiguration.getKeepAliveInterval() > 0) { Connection.addConnectionCreationListener(new ConnectionCreationListener() { public void connectionCreated(Connection connection) { - new ServerPingManager(connection); + new KeepAliveManager(connection); } }); } @@ -87,56 +72,94 @@ public class ServerPingManager { private volatile long lastSuccessfulContact = -1; /** - * Retrieves a {@link ServerPingManager} for the specified {@link Connection}, creating one if it doesn't already + * Retrieves a {@link KeepAliveManager} for the specified {@link Connection}, creating one if it doesn't already * exist. * * @param connection * The connection the manager is attached to. * @return The new or existing manager. */ - public synchronized static ServerPingManager getInstanceFor(Connection connection) { - ServerPingManager pingManager = instances.get(connection); + public synchronized static KeepAliveManager getInstanceFor(Connection connection) { + KeepAliveManager pingManager = instances.get(connection); if (pingManager == null) { - pingManager = new ServerPingManager(connection); + pingManager = new KeepAliveManager(connection); + instances.put(connection, pingManager); } return pingManager; } - private ServerPingManager(Connection connection) { - ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); - sdm.addFeature(Ping.NAMESPACE); + /* + * Start the executor service if it hasn't been started yet. + */ + private synchronized static void enableExecutorService() { + if (periodicPingExecutorService == null) { + periodicPingExecutorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() { + @Override + public Thread newThread(Runnable runnable) { + Thread pingThread = new Thread(runnable, "Smack Keepalive"); + pingThread.setDaemon(true); + return pingThread; + } + }); + } + } + + /* + * Stop the executor service if all monitored connections are disconnected. + */ + private synchronized static void handleDisconnect(Connection con) { + if (periodicPingExecutorService != null) { + instances.remove(con); + + if (instances.isEmpty()) { + periodicPingExecutorService.shutdownNow(); + periodicPingExecutorService = null; + } + } + } + + private KeepAliveManager(Connection connection) { this.connection = connection; init(); + handleConnect(); + } + + /* + * Call after every connection to add the packet listener. + */ + private void handleConnect() { + // Listen for all incoming packets and reset the scheduled ping whenever + // one arrives. + connection.addPacketListener(new PacketListener() { + + @Override + public void processPacket(Packet packet) { + // reschedule the ping based on this last server contact + lastSuccessfulContact = System.currentTimeMillis(); + schedulePingServerTask(); + } + }, null); } private void init() { - PacketFilter pingPacketFilter = new AndFilter(new PacketTypeFilter(Ping.class), new IQTypeFilter(Type.GET)); - - connection.addPacketListener(new PacketListener() { - /** - * Sends a Pong for every Ping - */ - public void processPacket(Packet packet) { - IQ pong = IQ.createResultIQ((Ping) packet); - connection.sendPacket(pong); - } - }, pingPacketFilter); - connection.addConnectionListener(new ConnectionListener() { @Override public void connectionClosed() { stopPingServerTask(); + handleDisconnect(connection); } @Override public void connectionClosedOnError(Exception arg0) { stopPingServerTask(); + handleDisconnect(connection); } @Override public void reconnectionSuccessful() { + handleConnect(); schedulePingServerTask(); } @@ -149,17 +172,6 @@ public class ServerPingManager { } }); - // Listen for all incoming packets and reset the scheduled ping whenever - // one arrives. - connection.addPacketListener(new PacketListener() { - - @Override - public void processPacket(Packet packet) { - // reschedule the ping based on this last server contact - lastSuccessfulContact = System.currentTimeMillis(); - schedulePingServerTask(); - } - }, null); instances.put(connection, this); schedulePingServerTask(); } @@ -171,15 +183,20 @@ public class ServerPingManager { * The new ping time interval in milliseconds. */ public void setPingInterval(long newPingInterval) { - if (pingInterval != newPingInterval) { - pingInterval = newPingInterval; + if (pingInterval == newPingInterval) + return; + + // Enable the executor service + if (newPingInterval > 0) + enableExecutorService(); + + pingInterval = newPingInterval; - if (pingInterval < 0) { - stopPinging(); - } - else { - schedulePingServerTask(); - } + if (pingInterval < 0) { + stopPinging(); + } + else { + schedulePingServerTask(); } } @@ -227,12 +244,20 @@ public class ServerPingManager { } /** - * Returns the time of the last successful contact with the server. (i.e. the last time any message was received). + * Returns the elapsed time (in milliseconds) since the last successful contact with the server + * (i.e. the last time any message was received). + *

    + * Note: Result is -1 if no message has been received since manager was created and + * 0 if the elapsed time is negative due to a clock reset. * - * @return Time of last message or -1 if none has been received since manager was created. + * @return Elapsed time since last message was received. */ - public long getLastSuccessfulContact() { - return lastSuccessfulContact; + public long getTimeSinceLastContact() { + if (lastSuccessfulContact == -1) + return lastSuccessfulContact; + long delta = System.currentTimeMillis() - lastSuccessfulContact; + + return (delta < 0) ? 0 : delta; } /** @@ -242,6 +267,7 @@ public class ServerPingManager { * This is designed so only one executor is used for scheduling all pings on all connections. This results in only 1 thread used for pinging. */ private synchronized void schedulePingServerTask() { + enableExecutorService(); stopPingServerTask(); if (pingInterval > 0) { diff --git a/source/org/jivesoftware/smackx/ping/PingManager.java b/source/org/jivesoftware/smackx/ping/PingManager.java index 9ab6f026e..2da9dcadc 100644 --- a/source/org/jivesoftware/smackx/ping/PingManager.java +++ b/source/org/jivesoftware/smackx/ping/PingManager.java @@ -16,11 +16,25 @@ package org.jivesoftware.smackx.ping; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ScheduledExecutorService; + import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.PacketListener; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.SmackError; import org.jivesoftware.smack.XMPPException; -import org.jivesoftware.smack.ping.ServerPingManager; +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.keepalive.KeepAliveManager; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.IQ.Type; import org.jivesoftware.smack.ping.packet.Ping; import org.jivesoftware.smack.util.SyncPacketSend; import org.jivesoftware.smackx.ServiceDiscoveryManager; @@ -31,7 +45,7 @@ import org.jivesoftware.smackx.packet.DiscoverInfo; * allows one entity to 'ping' any other entity by simply sending a ping to * the appropriate JID. *

    - * NOTE: The {@link ServerPingManager} already provides a keepalive functionality + * NOTE: The {@link KeepAliveManager} already provides a keepalive functionality * for regularly pinging the server to keep the underlying transport connection * alive. This class is specifically intended to do manual pings of other * entities. @@ -41,12 +55,57 @@ import org.jivesoftware.smackx.packet.DiscoverInfo; * Ping */ public class PingManager { + private static Map instances = Collections + .synchronizedMap(new WeakHashMap()); + + static { + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + public void connectionCreated(Connection connection) { + new PingManager(connection); + } + }); + } + private Connection connection; - public PingManager(Connection connection) { + /** + * Retrieves a {@link PingManager} for the specified {@link Connection}, creating one if it doesn't already + * exist. + * + * @param connection + * The connection the manager is attached to. + * @return The new or existing manager. + */ + public synchronized static PingManager getInstanceFor(Connection connection) { + PingManager pingManager = instances.get(connection); + + if (pingManager == null) { + pingManager = new PingManager(connection); + } + return pingManager; + } + + private PingManager(Connection con) { + this.connection = con; ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); + + // The ServiceDiscoveryManager was not pre-initialized + if (sdm == null) + sdm = new ServiceDiscoveryManager(connection); + sdm.addFeature(Ping.NAMESPACE); - this.connection = connection; + + PacketFilter pingPacketFilter = new AndFilter(new PacketTypeFilter(Ping.class), new IQTypeFilter(Type.GET)); + + connection.addPacketListener(new PacketListener() { + /** + * Sends a Pong for every Ping + */ + public void processPacket(Packet packet) { + IQ pong = IQ.createResultIQ((Ping) packet); + connection.sendPacket(pong); + } + }, pingPacketFilter); } /** diff --git a/test-unit/org/jivesoftware/smack/DummyConnection.java b/test-unit/org/jivesoftware/smack/DummyConnection.java index c144eff5a..57753a1ba 100644 --- a/test-unit/org/jivesoftware/smack/DummyConnection.java +++ b/test-unit/org/jivesoftware/smack/DummyConnection.java @@ -49,6 +49,7 @@ public class DummyConnection extends Connection { private boolean authenticated = false; private boolean anonymous = false; + private boolean reconnect = false; private String user; private String connectionID; @@ -71,6 +72,12 @@ public class DummyConnection extends Connection { @Override public void connect() throws XMPPException { connectionID = "dummy-" + new Random(new Date().getTime()).nextInt(); + + if (reconnect) { + for (ConnectionListener listener : getConnectionListeners()) { + listener.reconnectionSuccessful(); + } + } } @Override @@ -80,6 +87,11 @@ public class DummyConnection extends Connection { roster = null; authenticated = false; anonymous = false; + + for (ConnectionListener listener : getConnectionListeners()) { + listener.connectionClosed(); + } + reconnect = true; } @Override diff --git a/test-unit/org/jivesoftware/smack/ping/KeepaliveTest.java b/test-unit/org/jivesoftware/smack/keepalive/KeepaliveTest.java similarity index 79% rename from test-unit/org/jivesoftware/smack/ping/KeepaliveTest.java rename to test-unit/org/jivesoftware/smack/keepalive/KeepaliveTest.java index 25927d4e8..937bcebbb 100644 --- a/test-unit/org/jivesoftware/smack/ping/KeepaliveTest.java +++ b/test-unit/org/jivesoftware/smack/keepalive/KeepaliveTest.java @@ -1,4 +1,4 @@ -package org.jivesoftware.smack.ping; +package org.jivesoftware.smack.keepalive; import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; import static org.junit.Assert.assertEquals; @@ -17,8 +17,10 @@ import org.jivesoftware.smack.TestUtils; import org.jivesoftware.smack.ThreadedDummyConnection; import org.jivesoftware.smack.filter.IQTypeFilter; import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.keepalive.KeepAliveManager; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.ping.PingFailedListener; import org.jivesoftware.smack.ping.packet.Ping; import org.jivesoftware.smack.util.PacketParserUtils; import org.junit.After; @@ -40,6 +42,7 @@ public class KeepaliveTest { @Before public void resetProperties() { + SmackConfiguration.setKeepAliveInterval(-1); originalTimeout = SmackConfiguration.getPacketReplyTimeout(); SmackConfiguration.setPacketReplyTimeout(1000); } @@ -56,7 +59,7 @@ public class KeepaliveTest { public void validatePingStanzaXML() throws Exception { // @formatter:off String control = "" - + "" + ""; + + ""; // @formatter:on Ping ping = new Ping(TO); @@ -65,29 +68,6 @@ public class KeepaliveTest { assertXMLEqual(control, ping.toXML()); } - @Test - public void checkProvider() throws Exception { - // @formatter:off - String control = "" - + "" + ""; - // @formatter:on - DummyConnection con = new DummyConnection(); - IQ pingRequest = PacketParserUtils.parseIQ(TestUtils.getIQParser(control), con); - - assertTrue(pingRequest instanceof Ping); - - con.processPacket(pingRequest); - - Packet pongPacket = con.getSentPacket(); - assertTrue(pongPacket instanceof IQ); - - IQ pong = (IQ) pongPacket; - assertEquals("juliet@capulet.lit/balcony", pong.getFrom()); - assertEquals("capulet.lit", pong.getTo()); - assertEquals("s2c1", pong.getPacketID()); - assertEquals(IQ.Type.RESULT, pong.getType()); - } - @Test public void serverPingFailSingleConnection() throws Exception { DummyConnection connection = getConnection(); @@ -138,7 +118,7 @@ public class KeepaliveTest { } private void addPingFailedListener(DummyConnection con, final CountDownLatch latch) { - ServerPingManager manager = ServerPingManager.getInstanceFor(con); + KeepAliveManager manager = KeepAliveManager.getInstanceFor(con); manager.addPingFailedListener(new PingFailedListener() { @Override public void pingFailed() { @@ -149,7 +129,7 @@ public class KeepaliveTest { private DummyConnection getConnection() { DummyConnection con = new DummyConnection(); - ServerPingManager mgr = ServerPingManager.getInstanceFor(con); + KeepAliveManager mgr = KeepAliveManager.getInstanceFor(con); mgr.setPingInterval(PING_MINIMUM); return con; @@ -157,7 +137,7 @@ public class KeepaliveTest { private ThreadedDummyConnection getThreadedConnection() { ThreadedDummyConnection con = new ThreadedDummyConnection(); - ServerPingManager mgr = ServerPingManager.getInstanceFor(con); + KeepAliveManager mgr = KeepAliveManager.getInstanceFor(con); mgr.setPingInterval(PING_MINIMUM); return con; diff --git a/test-unit/org/jivesoftware/smackx/ping/PingTest.java b/test-unit/org/jivesoftware/smackx/ping/PingTest.java index b7b6358b8..c127f18dc 100644 --- a/test-unit/org/jivesoftware/smackx/ping/PingTest.java +++ b/test-unit/org/jivesoftware/smackx/ping/PingTest.java @@ -38,10 +38,37 @@ public class PingTest { threadedCon = new ThreadedDummyConnection(); } + @Test + public void checkProvider() throws Exception { + // @formatter:off + String control = "" + + "" + + ""; + // @formatter:on + DummyConnection con = new DummyConnection(); + + // Enable ping for this connection + PingManager.getInstanceFor(con); + IQ pingRequest = PacketParserUtils.parseIQ(TestUtils.getIQParser(control), con); + + assertTrue(pingRequest instanceof Ping); + + con.processPacket(pingRequest); + + Packet pongPacket = con.getSentPacket(); + assertTrue(pongPacket instanceof IQ); + + IQ pong = (IQ) pongPacket; + assertEquals("juliet@capulet.lit/balcony", pong.getFrom()); + assertEquals("capulet.lit", pong.getTo()); + assertEquals("s2c1", pong.getPacketID()); + assertEquals(IQ.Type.RESULT, pong.getType()); + } + @Test public void checkSendingPing() throws Exception { dummyCon = new DummyConnection(); - PingManager pinger = new PingManager(dummyCon); + PingManager pinger = PingManager.getInstanceFor(dummyCon); pinger.ping("test@myserver.com"); Packet sentPacket = dummyCon.getSentPacket(); @@ -54,7 +81,7 @@ public class PingTest { public void checkSuccessfulPing() throws Exception { threadedCon = new ThreadedDummyConnection(); - PingManager pinger = new PingManager(threadedCon); + PingManager pinger = PingManager.getInstanceFor(threadedCon); boolean pingSuccess = pinger.ping("test@myserver.com"); @@ -69,7 +96,7 @@ public class PingTest { @Test public void checkFailedPingOnTimeout() throws Exception { dummyCon = new DummyConnection(); - PingManager pinger = new PingManager(dummyCon); + PingManager pinger = PingManager.getInstanceFor(dummyCon); boolean pingSuccess = pinger.ping("test@myserver.com"); @@ -96,7 +123,7 @@ public class PingTest { IQ serviceUnavailable = PacketParserUtils.parseIQ(TestUtils.getIQParser(reply), threadedCon); threadedCon.addIQReply(serviceUnavailable); - PingManager pinger = new PingManager(threadedCon); + PingManager pinger = PingManager.getInstanceFor(threadedCon); boolean pingSuccess = pinger.ping("test@myserver.com"); @@ -106,7 +133,7 @@ public class PingTest { @Test public void checkPingToServerSuccess() throws Exception { ThreadedDummyConnection con = new ThreadedDummyConnection(); - PingManager pinger = new PingManager(con); + PingManager pinger = PingManager.getInstanceFor(con); boolean pingSuccess = pinger.pingMyServer(); @@ -132,7 +159,7 @@ public class PingTest { IQ serviceUnavailable = PacketParserUtils.parseIQ(TestUtils.getIQParser(reply), con); con.addIQReply(serviceUnavailable); - PingManager pinger = new PingManager(con); + PingManager pinger = PingManager.getInstanceFor(con); boolean pingSuccess = pinger.pingMyServer(); @@ -142,7 +169,7 @@ public class PingTest { @Test public void checkPingToServerTimeout() throws Exception { DummyConnection con = new DummyConnection(); - PingManager pinger = new PingManager(con); + PingManager pinger = PingManager.getInstanceFor(con); boolean pingSuccess = pinger.pingMyServer(); @@ -165,7 +192,7 @@ public class PingTest { IQ discoReply = PacketParserUtils.parseIQ(TestUtils.getIQParser(reply), con); con.addIQReply(discoReply); - PingManager pinger = new PingManager(con); + PingManager pinger = PingManager.getInstanceFor(con); boolean pingSupported = pinger.isPingSupported("test@myserver.com"); assertTrue(pingSupported); @@ -187,7 +214,7 @@ public class PingTest { IQ discoReply = PacketParserUtils.parseIQ(TestUtils.getIQParser(reply), con); con.addIQReply(discoReply); - PingManager pinger = new PingManager(con); + PingManager pinger = PingManager.getInstanceFor(con); boolean pingSupported = pinger.isPingSupported("test@myserver.com"); assertFalse(pingSupported); From 9f0f676151389cf4db676f68bbaa68ee9463d9ac Mon Sep 17 00:00:00 2001 From: rcollier Date: Tue, 16 Apr 2013 01:50:41 +0000 Subject: [PATCH 31/41] SMACK-403 Pulling this contribution until the related specs reach a draft status git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13611 b35dd754-fafc-0310-a699-88a17e54d16e --- .../jivesoftware/smackx/carbons/Carbon.java | 139 ------------ .../smackx/carbons/CarbonManager.java | 213 ------------------ .../smackx/forward/Forwarded.java | 125 ---------- 3 files changed, 477 deletions(-) delete mode 100644 source/org/jivesoftware/smackx/carbons/Carbon.java delete mode 100644 source/org/jivesoftware/smackx/carbons/CarbonManager.java delete mode 100644 source/org/jivesoftware/smackx/forward/Forwarded.java diff --git a/source/org/jivesoftware/smackx/carbons/Carbon.java b/source/org/jivesoftware/smackx/carbons/Carbon.java deleted file mode 100644 index 588688aa1..000000000 --- a/source/org/jivesoftware/smackx/carbons/Carbon.java +++ /dev/null @@ -1,139 +0,0 @@ -/** - * 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 - * XEP-0280 is - * meant to synchronize a message flow to multiple presences of a user. - * - *

    The {@link Carbon.Provider} must be registered in the - * smack.properties file for the elements sent and - * received with namespace urn:xmpp:carbons:2

    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(""); - 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 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/source/org/jivesoftware/smackx/carbons/CarbonManager.java b/source/org/jivesoftware/smackx/carbons/CarbonManager.java deleted file mode 100644 index f44701afd..000000000 --- a/source/org/jivesoftware/smackx/carbons/CarbonManager.java +++ /dev/null @@ -1,213 +0,0 @@ -/** - * 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 instances = - Collections.synchronizedMap(new WeakHashMap()); - - 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/source/org/jivesoftware/smackx/forward/Forwarded.java b/source/org/jivesoftware/smackx/forward/Forwarded.java deleted file mode 100644 index 817ee27db..000000000 --- a/source/org/jivesoftware/smackx/forward/Forwarded.java +++ /dev/null @@ -1,125 +0,0 @@ -/** - * 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 - * XEP-0297 is - * a prerequisite for XEP-0280 (Message Carbons). - * - *

    The {@link Forwarded.Provider} must be registered in the - * smack.properties file for the element forwarded with - * namespace urn:xmpp:forwarded:0

    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(""); - 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); - } - } -} From a3faa41696f83d40b71211dc917fba4e84a2b9ff Mon Sep 17 00:00:00 2001 From: rcollier Date: Tue, 16 Apr 2013 02:19:38 +0000 Subject: [PATCH 32/41] SMACK-403 Pulling this contribution until the related specs reach a draft status git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13612 b35dd754-fafc-0310-a699-88a17e54d16e --- build/resources/META-INF/smack.providers | 26 --- .../smackx/carbons/CarbonForwardedTest.java | 168 ------------------ 2 files changed, 194 deletions(-) delete mode 100644 test-unit/org/jivesoftware/smackx/carbons/CarbonForwardedTest.java diff --git a/build/resources/META-INF/smack.providers b/build/resources/META-INF/smack.providers index 28be01c7b..b1ce64057 100644 --- a/build/resources/META-INF/smack.providers +++ b/build/resources/META-INF/smack.providers @@ -641,32 +641,6 @@ org.jivesoftware.smackx.packet.AttentionExtension$Provider - - - forwarded - urn:xmpp:forward:0 - org.jivesoftware.smackx.forward.Forwarded$Provider - - - - - sent - urn:xmpp:carbons:2 - org.jivesoftware.smackx.carbons.Carbon$Provider - - - received - urn:xmpp:carbons:2 - org.jivesoftware.smackx.carbons.Carbon$Provider - - - - - ping - urn:xmpp:ping - org.jivesoftware.smackx.ping.provider.PingProvider - - received diff --git a/test-unit/org/jivesoftware/smackx/carbons/CarbonForwardedTest.java b/test-unit/org/jivesoftware/smackx/carbons/CarbonForwardedTest.java deleted file mode 100644 index b8d3d83a1..000000000 --- a/test-unit/org/jivesoftware/smackx/carbons/CarbonForwardedTest.java +++ /dev/null @@ -1,168 +0,0 @@ -/** - * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF 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 static org.junit.Assert.*; - -import java.io.IOException; -import java.io.StringReader; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.Properties; -import java.util.TimeZone; - -import org.jivesoftware.smack.packet.Packet; -import org.jivesoftware.smackx.packet.DelayInfo; -import org.jivesoftware.smackx.packet.DelayInformation; -import org.jivesoftware.smackx.forward.Forwarded; -import org.junit.Test; -import org.xmlpull.mxp1.MXParser; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import com.jamesmurty.utils.XMLBuilder; - -public class CarbonForwardedTest { - - private static Properties outputProperties = new Properties(); - static { - outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); - } - - @Test - public void forwardedTest() throws Exception { - XmlPullParser parser; - String control; - Forwarded fwd; - - control = XMLBuilder.create("forwarded") - .a("xmlns", "urn:xmpp:forwarded:0") - .e("message") - .a("from", "romeo@montague.com") - .asString(outputProperties); - - parser = getParser(control, "forwarded"); - fwd = (Forwarded) new Forwarded.Provider().parseExtension(parser); - - // no delay in packet - assertEquals(null, fwd.getDelayInfo()); - - // check message - assertEquals("romeo@montague.com", fwd.getForwardedPacket().getFrom()); - - // check end of tag - assertEquals(XmlPullParser.END_TAG, parser.getEventType()); - assertEquals("forwarded", parser.getName()); - - } - - @Test(expected=Exception.class) - public void forwardedEmptyTest() throws Exception { - XmlPullParser parser; - String control; - - control = XMLBuilder.create("forwarded") - .a("xmlns", "urn:xmpp:forwarded:0") - .asString(outputProperties); - - parser = getParser(control, "forwarded"); - new Forwarded.Provider().parseExtension(parser); - } - - @Test - public void carbonSentTest() throws Exception { - XmlPullParser parser; - String control; - Carbon cc; - Forwarded fwd; - - control = XMLBuilder.create("sent") - .e("forwarded") - .a("xmlns", "urn:xmpp:forwarded:0") - .e("message") - .a("from", "romeo@montague.com") - .asString(outputProperties); - - parser = getParser(control, "sent"); - cc = (Carbon) new Carbon.Provider().parseExtension(parser); - fwd = cc.getForwarded(); - - // meta - assertEquals(Carbon.Direction.sent, cc.getDirection()); - - // no delay in packet - assertEquals(null, fwd.getDelayInfo()); - - // check message - assertEquals("romeo@montague.com", fwd.getForwardedPacket().getFrom()); - - // check end of tag - assertEquals(XmlPullParser.END_TAG, parser.getEventType()); - assertEquals("sent", parser.getName()); - } - - @Test - public void carbonReceivedTest() throws Exception { - XmlPullParser parser; - String control; - Carbon cc; - - control = XMLBuilder.create("received") - .e("forwarded") - .a("xmlns", "urn:xmpp:forwarded:0") - .e("message") - .a("from", "romeo@montague.com") - .asString(outputProperties); - - parser = getParser(control, "received"); - cc = (Carbon) new Carbon.Provider().parseExtension(parser); - - assertEquals(Carbon.Direction.received, cc.getDirection()); - - // check end of tag - assertEquals(XmlPullParser.END_TAG, parser.getEventType()); - assertEquals("received", parser.getName()); - } - - @Test(expected=Exception.class) - public void carbonEmptyTest() throws Exception { - XmlPullParser parser; - String control; - - control = XMLBuilder.create("sent") - .a("xmlns", "urn:xmpp:forwarded:0") - .asString(outputProperties); - - parser = getParser(control, "sent"); - new Carbon.Provider().parseExtension(parser); - } - - private XmlPullParser getParser(String control, String startTag) - throws XmlPullParserException, IOException { - XmlPullParser parser = new MXParser(); - parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); - parser.setInput(new StringReader(control)); - - while (true) { - if (parser.next() == XmlPullParser.START_TAG - && parser.getName().equals(startTag)) { - break; - } - } - return parser; - } - -} From 13d87e3d40bf9fb1356eba5eed591ce665d05a50 Mon Sep 17 00:00:00 2001 From: rcollier Date: Sat, 20 Apr 2013 21:55:27 +0000 Subject: [PATCH 33/41] SMACK-361 Changed default for filename encoding to Base 32 (which should work on all file systems) and added a Base 64 filename and url safe implementation of the StringEncoder. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13619 b35dd754-fafc-0310-a699-88a17e54d16e --- .../org/jivesoftware/smack/util/Base64.java | 88 +------------------ .../smack/util/Base64Encoder.java | 1 + .../smack/util/Base64FileUrlEncoder.java | 48 ++++++++++ .../cache/SimpleDirectoryPersistentCache.java | 27 +++--- .../entitycaps/EntityCapsManagerTest.java | 3 +- 5 files changed, 67 insertions(+), 100 deletions(-) create mode 100644 source/org/jivesoftware/smack/util/Base64FileUrlEncoder.java diff --git a/source/org/jivesoftware/smack/util/Base64.java b/source/org/jivesoftware/smack/util/Base64.java index 940c55118..ba6eb371f 100644 --- a/source/org/jivesoftware/smack/util/Base64.java +++ b/source/org/jivesoftware/smack/util/Base64.java @@ -7,66 +7,9 @@ package org.jivesoftware.smack.util; /** - *

    Encodes and decodes to and from Base64 notation.

    - *

    Homepage: http://iharder.net/base64.

    + *

    Encodes and decodes to and from Base64 notation.

    + * This code was obtained from http://iharder.net/base64

    * - *

    - * Change Log: - *

    - *
      - *
    • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug - * when using very small files (~< 40 bytes).
    • - *
    • v2.2 - Added some helper methods for encoding/decoding directly from - * one file to the next. Also added a main() method to support command line - * encoding/decoding from one file to the next. Also added these Base64 dialects: - *
        - *
      1. The default is RFC3548 format.
      2. - *
      3. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates - * URL and file name friendly format as described in Section 4 of RFC3548. - * http://www.faqs.org/rfcs/rfc3548.html
      4. - *
      5. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates - * URL and file name friendly format that preserves lexical ordering as described - * in http://www.faqs.org/qa/rfcc-1940.html
      6. - *
      - * Special thanks to Jim Kellerman at http://www.powerset.com/ - * for contributing the new Base64 dialects. - *
    • - * - *
    • v2.1 - Cleaned up javadoc comments and unused variables and methods. Added - * some convenience methods for reading and writing to and from files.
    • - *
    • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems - * with other encodings (like EBCDIC).
    • - *
    • v2.0.1 - Fixed an error when decoding a single byte, that is, when the - * encoded data was a single byte.
    • - *
    • v2.0 - I got rid of methods that used booleans to set options. - * Now everything is more consolidated and cleaner. The code now detects - * when data that's being decoded is gzip-compressed and will decompress it - * automatically. Generally things are cleaner. You'll probably have to - * change some method calls that you were making to support the new - * options format (ints that you "OR" together).
    • - *
    • v1.5.1 - Fixed bug when decompressing and decoding to a - * byte[] using decode( String s, boolean gzipCompressed ). - * Added the ability to "suspend" encoding in the Output Stream so - * you can turn on and off the encoding if you need to embed base64 - * data in an otherwise "normal" stream (like an XML file).
    • - *
    • v1.5 - Output stream pases on flush() command but doesn't do anything itself. - * This helps when using GZIP streams. - * Added the ability to GZip-compress objects before encoding them.
    • - *
    • v1.4 - Added helper methods to read/write files.
    • - *
    • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
    • - *
    • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream - * where last buffer being read, if not completely full, was not returned.
    • - *
    • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
    • - *
    • v1.3.3 - Fixed I/O streams which were totally messed up.
    • - *
    - * - *

    - * I am placing this code in the Public Domain. Do with it as you will. - * This software comes with no guarantees or warranties but with - * plenty of well-wishing instead! - * Please visit http://iharder.net/base64 - * periodically to check for updates or to contribute improvements. - *

    * * @author Robert Harder * @author rob@iharder.net @@ -368,33 +311,6 @@ public class Base64 /** Defeats instantiation. */ private Base64(){} - - /** - * Encodes or decodes two files from the command line; - * feel free to delete this method (in fact you probably should) - * if you're embedding this code into a larger program. - */ - public final static void main( String[] args ) - { - if( args.length < 3 ){ - usage("Not enough arguments."); - } // end if: args.length < 3 - else { - String flag = args[0]; - String infile = args[1]; - String outfile = args[2]; - if( flag.equals( "-e" ) ){ - Base64.encodeFileToFile( infile, outfile ); - } // end if: encode - else if( flag.equals( "-d" ) ) { - Base64.decodeFileToFile( infile, outfile ); - } // end else if: decode - else { - usage( "Unknown flag: " + flag ); - } // end else - } // end else - } // end main - /** * Prints command line usage. * diff --git a/source/org/jivesoftware/smack/util/Base64Encoder.java b/source/org/jivesoftware/smack/util/Base64Encoder.java index 8d29f12c5..d53c0ed08 100644 --- a/source/org/jivesoftware/smack/util/Base64Encoder.java +++ b/source/org/jivesoftware/smack/util/Base64Encoder.java @@ -16,6 +16,7 @@ package org.jivesoftware.smack.util; /** + * A Base 64 encoding implementation. * @author Florian Schmaus */ public class Base64Encoder implements StringEncoder { diff --git a/source/org/jivesoftware/smack/util/Base64FileUrlEncoder.java b/source/org/jivesoftware/smack/util/Base64FileUrlEncoder.java new file mode 100644 index 000000000..190b37439 --- /dev/null +++ b/source/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. + * + *

    + * Note: This does NOT produce standard Base 64 encodings, but a variant as defined in + * Section 4 of RFC3548: + * http://www.faqs.org/rfcs/rfc3548.html. + * + * @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/source/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java b/source/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java index 329e4dce6..ae0e11633 100644 --- a/source/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java +++ b/source/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java @@ -27,6 +27,7 @@ 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; @@ -47,19 +48,20 @@ import org.xmlpull.v1.XmlPullParserException; public class SimpleDirectoryPersistentCache implements EntityCapsPersistentCache { private File cacheDir; - private StringEncoder stringEncoder; + private StringEncoder filenameEncoder; /** * 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 Base32. + *

    + * 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, Base64Encoder.getInstance()); + this(cacheDir, Base32Encoder.getInstance()); } /** @@ -67,26 +69,25 @@ public class SimpleDirectoryPersistentCache implements EntityCapsPersistentCache * cacheDir exists and that it's an directory. * * If your cacheDir is case insensitive then make sure to set the - * StringEncoder to Base32. + * StringEncoder to {@link Base32Encoder} (which is the default). * - * @param cacheDir - * @param stringEncoder + * @param cacheDir The directory where the cache will be stored. + * @param filenameEncoder Encodes the node string into a filename. */ - public SimpleDirectoryPersistentCache(File cacheDir, StringEncoder stringEncoder) { + 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.stringEncoder = stringEncoder; + this.filenameEncoder = filenameEncoder; } @Override public void addDiscoverInfoByNodePersistent(String node, DiscoverInfo info) { - String filename = stringEncoder.encode(node); + String filename = filenameEncoder.encode(node); File nodeFile = new File(cacheDir, filename); - try { if (nodeFile.createNewFile()) writeInfoToFile(nodeFile, info); @@ -99,7 +100,7 @@ public class SimpleDirectoryPersistentCache implements EntityCapsPersistentCache public void replay() throws IOException { File[] files = cacheDir.listFiles(); for (File f : files) { - String node = stringEncoder.decode(f.getName()); + String node = filenameEncoder.decode(f.getName()); DiscoverInfo info = restoreInfoFromFile(f); if (info == null) continue; diff --git a/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java b/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java index 5ae4642a9..158fe466b 100644 --- a/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java +++ b/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java @@ -10,6 +10,7 @@ import java.util.LinkedList; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.util.Base32Encoder; import org.jivesoftware.smack.util.Base64Encoder; +import org.jivesoftware.smack.util.Base64FileUrlEncoder; import org.jivesoftware.smack.util.StringEncoder; import org.jivesoftware.smackx.FormField; import org.jivesoftware.smackx.entitycaps.EntityCapsManager; @@ -37,7 +38,7 @@ public class EntityCapsManagerTest { @Test public void testSimpleDirectoryCacheBase64() throws IOException { EntityCapsManager.persistentCache = null; - testSimpleDirectoryCache(Base64Encoder.getInstance()); + testSimpleDirectoryCache(Base64FileUrlEncoder.getInstance()); } @Test From fe2e9cdb76e5df256eb5cdac2d813137e3ed4536 Mon Sep 17 00:00:00 2001 From: rcollier Date: Sat, 20 Apr 2013 22:05:28 +0000 Subject: [PATCH 34/41] SMACK-344 Patch applied that allows the host and service name to be different for SASL authentication git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13620 b35dd754-fafc-0310-a699-88a17e54d16e --- .../smack/SASLAuthentication.java | 11 +- .../smack/sasl/SASLMechanism.java | 101 ++++++++++++++++-- 2 files changed, 99 insertions(+), 13 deletions(-) diff --git a/source/org/jivesoftware/smack/SASLAuthentication.java b/source/org/jivesoftware/smack/SASLAuthentication.java index a7e1dcbba..6f8b5bf1e 100644 --- a/source/org/jivesoftware/smack/SASLAuthentication.java +++ b/source/org/jivesoftware/smack/SASLAuthentication.java @@ -315,8 +315,13 @@ public class SASLAuthentication implements UserAuthentication { 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); + // may not match the XMPP domain. + + //The serviceName is basically the value that XMPP server sends to the client as being the location + //of the XMPP service we are trying to connect to. This should have the format: host [ "/" serv-name ] + //as per RFC-2831 guidelines + String serviceName = connection.getServiceName(); + currentMechanism.authenticate(username, connection.getHost(), serviceName, password); // Wait until SASL negotiation finishes synchronized (this) { @@ -383,7 +388,7 @@ public class SASLAuthentication implements UserAuthentication { public String authenticateAnonymously() throws XMPPException { try { currentMechanism = new SASLAnonymous(this); - currentMechanism.authenticate(null,null,""); + currentMechanism.authenticate(null,null,null,""); // Wait until SASL negotiation finishes synchronized (this) { diff --git a/source/org/jivesoftware/smack/sasl/SASLMechanism.java b/source/org/jivesoftware/smack/sasl/SASLMechanism.java index 4fc139cbd..0d096f2a2 100644 --- a/source/org/jivesoftware/smack/sasl/SASLMechanism.java +++ b/source/org/jivesoftware/smack/sasl/SASLMechanism.java @@ -51,6 +51,30 @@ import javax.security.sasl.SaslException; * using the CallbackHandler method. *

  • {@link #challengeReceived(String)} -- Handle a challenge from the server.
  • * + * + * Basic XMPP SASL authentication steps: + * 1. Client authentication initialization, stanza sent to the server (Base64 encoded): + * + * 2. Server sends back to the client the challenge response (Base64 encoded) + * sample: + * realm=,nonce="OA6MG9tEQGm2hh",qop="auth",charset=utf-8,algorithm=md5-sess + * 3. The client responds back to the server (Base 64 encoded): + * sample: + * username=,realm=,nonce="OA6MG9tEQGm2hh", + * cnonce="OA6MHXh6VqTrRk",nc=00000001,qop=auth, + * digest-uri=, + * response=d388dad90d4bbd760a152321f2143af7, + * charset=utf-8, + * authzid= + * 4. The server evaluates if the user is present and contained in the REALM + * if successful it sends: (Base64 encoded) + * if not successful it sends: + * sample: + * + * cnNwYXV0aD1lYTQwZjYwMzM1YzQyN2I1NTI3Yjg0ZGJhYmNkZmZmZA== + * + * + * * @author Jay Kline */ @@ -62,37 +86,88 @@ public abstract class SASLMechanism implements CallbackHandler { protected String password; protected String hostname; - public SASLMechanism(SASLAuthentication saslAuthentication) { this.saslAuthentication = saslAuthentication; } /** * Builds and sends the auth stanza to the server. Note that this method of - * authentication is not recommended, since it is very inflexable. Use + * authentication is not recommended, since it is very inflexable. Use * {@link #authenticate(String, String, CallbackHandler)} whenever possible. - * + * + * Explanation of auth stanza: + * + * The client authentication stanza needs to include the digest-uri of the form: xmpp/serverName + * From RFC-2831: + * digest-uri = "digest-uri" "=" digest-uri-value + * digest-uri-value = serv-type "/" host [ "/" serv-name ] + * + * digest-uri: + * Indicates the principal name of the service with which the client + * wishes to connect, formed from the serv-type, host, and serv-name. + * For example, the FTP service + * on "ftp.example.com" would have a "digest-uri" value of "ftp/ftp.example.com"; the SMTP + * server from the example above would have a "digest-uri" value of + * "smtp/mail3.example.com/example.com". + * + * host: + * The DNS host name or IP address for the service requested. The DNS host name + * must be the fully-qualified canonical name of the host. The DNS host name is the + * preferred form; see notes on server processing of the digest-uri. + * + * serv-name: + * Indicates the name of the service if it is replicated. The service is + * considered to be replicated if the client's service-location process involves resolution + * using standard DNS lookup operations, and if these operations involve DNS records (such + * as SRV, or MX) which resolve one DNS name into a set of other DNS names. In this case, + * the initial name used by the client is the "serv-name", and the final name is the "host" + * component. For example, the incoming mail service for "example.com" may be replicated + * through the use of MX records stored in the DNS, one of which points at an SMTP server + * called "mail3.example.com"; it's "serv-name" would be "example.com", it's "host" would be + * "mail3.example.com". If the service is not replicated, or the serv-name is identical to + * the host, then the serv-name component MUST be omitted + * + * digest-uri verification is needed for ejabberd 2.0.3 and higher + * * @param username the username of the user being authenticated. - * @param host the hostname where the user account resides. + * @param host the hostname where the user account resides. + * @param serviceName the xmpp service location - used by the SASL client in digest-uri creation + * serviceName format is: host [ "/" serv-name ] as per RFC-2831 * @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 { + public void authenticate(String username, String host, String serviceName, 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; + this.hostname = host; String[] mechanisms = { getName() }; - Map props = new HashMap(); - sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props, this); + Map props = new HashMap(); + sc = Sasl.createSaslClient(mechanisms, username, "xmpp", serviceName, props, this); authenticate(); } + /** + * Same as {@link #authenticate(String, String, String, String)}, but with the hostname used as the serviceName. + *

    + * Kept for backward compatibility only. + * + * @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. + * @deprecated Please use {@link #authenticate(String, String, String, String)} instead. + */ + public void authenticate(String username, String host, String password) throws IOException, XMPPException { + authenticate(username, host, host, password); + } + /** * Builds and sends the auth stanza to the server. The callback handler will handle * any additional information, such as the authentication ID or realm, if it is needed. @@ -178,7 +253,13 @@ public abstract class SASLMechanism implements CallbackHandler { pcb.setPassword(password.toCharArray()); } else if(callbacks[i] instanceof RealmCallback) { RealmCallback rcb = (RealmCallback)callbacks[i]; - rcb.setText(hostname); + //Retrieve the REALM from the challenge response that the server returned when the client initiated the authentication + //exchange. If this value is not null or empty, *this value* has to be sent back to the server in the client's response + //to the server's challenge + String text = rcb.getDefaultText(); + //The SASL client (sc) created in smack uses rcb.getText when creating the negotiatedRealm to send it back to the server + //Make sure that this value matches the server's realm + rcb.setText(text); } else if(callbacks[i] instanceof RealmChoiceCallback){ //unused //RealmChoiceCallback rccb = (RealmChoiceCallback)callbacks[i]; @@ -319,5 +400,5 @@ public abstract class SASLMechanism implements CallbackHandler { stanza.append(""); return stanza.toString(); } - } + } } From ce0cb7d49168a19de4b800641f1443e6289d03ae Mon Sep 17 00:00:00 2001 From: rcollier Date: Mon, 22 Apr 2013 01:03:43 +0000 Subject: [PATCH 35/41] SMACK-433 Removed Thread.sleep usage for tests git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13621 b35dd754-fafc-0310-a699-88a17e54d16e --- .../jivesoftware/smack/ReconnectionTest.java | 99 ++-- .../smack/RosterListenerTest.java | 5 +- .../jivesoftware/smack/RosterSmackTest.java | 451 +++++++++--------- 3 files changed, 283 insertions(+), 272 deletions(-) diff --git a/test/org/jivesoftware/smack/ReconnectionTest.java b/test/org/jivesoftware/smack/ReconnectionTest.java index 25b5b7c87..4f2b080d0 100644 --- a/test/org/jivesoftware/smack/ReconnectionTest.java +++ b/test/org/jivesoftware/smack/ReconnectionTest.java @@ -17,7 +17,11 @@ */ package org.jivesoftware.smack; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + import org.jivesoftware.smack.test.SmackTestCase; +import org.jivesoftware.smackx.ping.PingManager; /** * Tests the connection and reconnection mechanism @@ -26,7 +30,9 @@ import org.jivesoftware.smack.test.SmackTestCase; */ public class ReconnectionTest extends SmackTestCase { - + + private static final long MIN_RECONNECT_WAIT = 17; // Seconds + public ReconnectionTest(String arg0) { super(arg0); } @@ -37,20 +43,18 @@ public class ReconnectionTest extends SmackTestCase { */ public void testAutomaticReconnection() throws Exception { - XMPPConnection connection = getConnection(0); - XMPPConnectionTestListener listener = new XMPPConnectionTestListener(); + XMPPConnection connection = getConnection(0); + CountDownLatch latch = new CountDownLatch(1); + XMPPConnectionTestListener listener = new XMPPConnectionTestListener(latch); connection.addConnectionListener(listener); // Simulates an error in the connection connection.notifyConnectionError(new Exception("Simulated Error")); - Thread.sleep(12000); + latch.await(MIN_RECONNECT_WAIT, TimeUnit.SECONDS); + // After 10 seconds, the reconnection manager must reestablishes the connection - assertEquals("The ConnectionListener.connectionStablished() notification was not fired", - true, listener.reconnected); - assertEquals("The ConnectionListener.reconnectingIn() notification was not fired", 10, - listener.attemptsNotifications); - assertEquals("The ReconnectionManager algorithm has reconnected without waiting until 0", 0, - listener.remainingSeconds); + assertEquals("The ConnectionListener.connectionStablished() notification was not fired", true, listener.reconnected); + assertTrue("The ReconnectionManager algorithm has reconnected without waiting at least 5 seconds", listener.attemptsNotifications > 0); // Executes some server interaction testing the connection executeSomeServerInteraction(connection); @@ -73,19 +77,17 @@ public class ReconnectionTest extends SmackTestCase { // Executes some server interaction testing the connection executeSomeServerInteraction(connection); - XMPPConnectionTestListener listener = new XMPPConnectionTestListener(); + CountDownLatch latch = new CountDownLatch(1); + XMPPConnectionTestListener listener = new XMPPConnectionTestListener(latch); connection.addConnectionListener(listener); // Simulates an error in the connection connection.notifyConnectionError(new Exception("Simulated Error")); - Thread.sleep(12000); + latch.await(MIN_RECONNECT_WAIT, TimeUnit.SECONDS); + // After 10 seconds, the reconnection manager must reestablishes the connection - assertEquals("The ConnectionListener.connectionStablished() notification was not fired", - true, listener.reconnected); - assertEquals("The ConnectionListener.reconnectingIn() notification was not fired", 10, - listener.attemptsNotifications); - assertEquals("The ReconnectionManager algorithm has reconnected without waiting until 0", 0, - listener.remainingSeconds); + assertEquals("The ConnectionListener.connectionEstablished() notification was not fired", true, listener.reconnected); + assertTrue("The ReconnectionManager algorithm has reconnected without waiting at least 5 seconds", listener.attemptsNotifications > 0); // Executes some server interaction testing the connection executeSomeServerInteraction(connection); @@ -97,7 +99,8 @@ public class ReconnectionTest extends SmackTestCase { */ public void testManualReconnectionWithCancelation() throws Exception { XMPPConnection connection = getConnection(0); - XMPPConnectionTestListener listener = new XMPPConnectionTestListener(); + CountDownLatch latch = new CountDownLatch(1); + XMPPConnectionTestListener listener = new XMPPConnectionTestListener(latch); connection.addConnectionListener(listener); // Produces a connection error @@ -105,14 +108,14 @@ public class ReconnectionTest extends SmackTestCase { assertEquals( "An error occurs but the ConnectionListener.connectionClosedOnError(e) was not notified", true, listener.connectionClosedOnError); - Thread.sleep(1000); +// Thread.sleep(1000); + // Cancels the automatic reconnection connection.getConfiguration().setReconnectionAllowed(false); // Waits for a reconnection that must not happened. - Thread.sleep(10500); + Thread.sleep(MIN_RECONNECT_WAIT * 1000); // Cancels the automatic reconnection - assertEquals("The connection was stablished but it was not allowed to", false, - listener.reconnected); + assertEquals(false, listener.reconnected); // Makes a manual reconnection from an error terminated connection without reconnection connection.connect(); @@ -137,7 +140,7 @@ public class ReconnectionTest extends SmackTestCase { assertEquals("ConnectionListener.connectionClosed() was not notified", true, listener.connectionClosed); // Waits 10 seconds waiting for a reconnection that must not happened. - Thread.sleep(12200); + Thread.sleep(MIN_RECONNECT_WAIT * 1000); assertEquals("The connection was stablished but it was not allowed to", false, listener.reconnected); @@ -187,8 +190,8 @@ public class ReconnectionTest extends SmackTestCase { * Execute some server interaction in order to test that the regenerated connection works fine. */ private void executeSomeServerInteraction(XMPPConnection connection) throws XMPPException { - PrivacyListManager privacyManager = PrivacyListManager.getInstanceFor(connection); - privacyManager.getPrivacyLists(); + PingManager pingManager = PingManager.getInstanceFor(connection); + pingManager.pingMyServer(); } protected int getMaxConnections() { @@ -198,41 +201,59 @@ public class ReconnectionTest extends SmackTestCase { private class XMPPConnectionTestListener implements ConnectionListener { // Variables to support listener notifications verification - private boolean connectionClosed = false; - private boolean connectionClosedOnError = false; - private boolean reconnected = false; - private boolean reconnectionFailed = false; - private int remainingSeconds = 0; - private int attemptsNotifications = 0; - private boolean reconnectionCanceled = false; - + private volatile boolean connectionClosed = false; + private volatile boolean connectionClosedOnError = false; + private volatile boolean reconnected = false; + private volatile boolean reconnectionFailed = false; + private volatile int remainingSeconds = 0; + private volatile int attemptsNotifications = 0; + private volatile boolean reconnectionCanceled = false; + private CountDownLatch countDownLatch; + + private XMPPConnectionTestListener(CountDownLatch latch) { + countDownLatch = latch; + } + + private XMPPConnectionTestListener() { + } /** * Methods to test the listener. */ - public void connectionClosed() { - connectionClosed = true; + public void connectionClosed() { + connectionClosed = true; + + if (countDownLatch != null) + countDownLatch.countDown(); } public void connectionClosedOnError(Exception e) { - connectionClosedOnError = true; + connectionClosedOnError = true; } public void reconnectionCanceled() { reconnectionCanceled = true; + + if (countDownLatch != null) + countDownLatch.countDown(); } public void reconnectingIn(int seconds) { attemptsNotifications = attemptsNotifications + 1; - remainingSeconds = seconds; - + remainingSeconds = seconds; } public void reconnectionSuccessful() { reconnected = true; + + if (countDownLatch != null) + countDownLatch.countDown(); } public void reconnectionFailed(Exception error) { reconnectionFailed = true; + + if (countDownLatch != null) + countDownLatch.countDown(); } } diff --git a/test/org/jivesoftware/smack/RosterListenerTest.java b/test/org/jivesoftware/smack/RosterListenerTest.java index 335a1545d..91ba11de9 100644 --- a/test/org/jivesoftware/smack/RosterListenerTest.java +++ b/test/org/jivesoftware/smack/RosterListenerTest.java @@ -20,6 +20,7 @@ package org.jivesoftware.smack; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.concurrent.CountDownLatch; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.test.SmackTestCase; @@ -48,8 +49,6 @@ public class RosterListenerTest extends SmackTestCase { // add user1 to roster to create roster events stored at XMPP server inviterRoster.createEntry(getBareJID(inviteeIndex), getUsername(inviteeIndex), null); - Thread.sleep(500); // wait for XMPP server - XMPPConnection inviteeConnection = getConnection(inviteeIndex); assertFalse("Invitee is already online", inviteeConnection.isConnected()); @@ -80,7 +79,7 @@ public class RosterListenerTest extends SmackTestCase { // connect after adding the listener connectAndLogin(inviteeIndex); - Thread.sleep(500); // wait for packets to be processed + Thread.sleep(5000); // wait for packets to be processed assertNotNull("inviter is not in roster", inviteeRoster.getEntry(getBareJID(inviterIndex))); diff --git a/test/org/jivesoftware/smack/RosterSmackTest.java b/test/org/jivesoftware/smack/RosterSmackTest.java index 4808c50f7..5d2aa97d1 100644 --- a/test/org/jivesoftware/smack/RosterSmackTest.java +++ b/test/org/jivesoftware/smack/RosterSmackTest.java @@ -21,12 +21,16 @@ package org.jivesoftware.smack; import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.test.SmackTestCase; import org.jivesoftware.smack.util.StringUtils; +import org.mockito.internal.util.RemoveFirstLine; /** * Tests the Roster functionality by creating and removing roster entries. @@ -43,6 +47,7 @@ public class RosterSmackTest extends SmackTestCase { super(name); } + /** * 1. Create entries in roster groups * 2. Iterate on the groups and remove the entry from each group @@ -52,63 +57,77 @@ public class RosterSmackTest extends SmackTestCase { try { // Add a new roster entry Roster roster = getConnection(0).getRoster(); + + CountDownLatch latch = new CountDownLatch(2); + setupCountdown(latch, roster); + roster.createEntry(getBareJID(1), "gato11", new String[] { "Friends", "Family" }); roster.createEntry(getBareJID(2), "gato12", new String[] { "Family" }); + + waitForCountdown(latch, roster, 2); - // Wait until the server confirms the new entries - long initial = System.currentTimeMillis(); - while (System.currentTimeMillis() - initial < 2000 && ( - !roster.getPresence(getBareJID(1)).isAvailable() || - !roster.getPresence(getBareJID(2)).isAvailable())) { - Thread.sleep(100); - } + final CountDownLatch removeLatch = new CountDownLatch(3); + RosterListener latchCounter = new RosterListener() { + @Override + public void presenceChanged(Presence presence) {} + + @Override + public void entriesUpdated(Collection addresses) { + removeLatch.countDown(); + } + + @Override + public void entriesDeleted(Collection addresses) {} + + @Override + public void entriesAdded(Collection addresses) {} + }; + + roster.addRosterListener(latchCounter); for (RosterEntry entry : roster.getEntries()) { for (RosterGroup rosterGroup : entry.getGroups()) { rosterGroup.removeEntry(entry); } } - // Wait up to 2 seconds - initial = System.currentTimeMillis(); - while (System.currentTimeMillis() - initial < 2000 && - (roster.getGroupCount() != 0 && - getConnection(2).getRoster().getEntryCount() != 2)) { - Thread.sleep(100); - } - - assertEquals( - "The number of entries in connection 1 should be 1", - 1, - getConnection(1).getRoster().getEntryCount()); - assertEquals( - "The number of groups in connection 1 should be 0", - 0, - getConnection(1).getRoster().getGroupCount()); - - assertEquals( - "The number of entries in connection 2 should be 1", - 1, - getConnection(2).getRoster().getEntryCount()); - assertEquals( - "The number of groups in connection 2 should be 0", - 0, - getConnection(2).getRoster().getGroupCount()); - - assertEquals( - "The number of entries in connection 0 should be 2", - 2, - roster.getEntryCount()); - assertEquals( - "The number of groups in connection 0 should be 0", - 0, - roster.getGroupCount()); + + removeLatch.await(5, TimeUnit.SECONDS); + roster.removeRosterListener(latchCounter); + + assertEquals("The number of entries in connection 1 should be 1", 1, getConnection(1).getRoster().getEntryCount()); + assertEquals("The number of groups in connection 1 should be 0", 0, getConnection(1).getRoster().getGroupCount()); + assertEquals("The number of entries in connection 2 should be 1", 1, getConnection(2).getRoster().getEntryCount()); + assertEquals("The number of groups in connection 2 should be 0", 0, getConnection(2).getRoster().getGroupCount()); + assertEquals("The number of entries in connection 0 should be 2", 2, roster.getEntryCount()); + assertEquals("The number of groups in connection 0 should be 0", 0, roster.getGroupCount()); } catch (Exception e) { fail(e.getMessage()); } - finally { - cleanUpRoster(); - } + } + + private void setupCountdown(final CountDownLatch latch, Roster roster) { + roster.addRosterListener(new RosterListener() { + + @Override + public void presenceChanged(Presence presence) {} + + @Override + public void entriesUpdated(Collection addresses) { + latch.countDown(); + } + + @Override + public void entriesDeleted(Collection addresses) {} + + @Override + public void entriesAdded(Collection addresses) {} + }); + } + + private void waitForCountdown(CountDownLatch latch, Roster roster, int entryCount) throws InterruptedException { + latch.await(5, TimeUnit.SECONDS); + assertEquals(entryCount, roster.getEntryCount()); } /** @@ -119,50 +138,30 @@ public class RosterSmackTest extends SmackTestCase { public void testDeleteAllRosterEntries() throws Exception { // Add a new roster entry Roster roster = getConnection(0).getRoster(); + + CountDownLatch latch = new CountDownLatch(2); + setupCountdown(latch, roster); + roster.createEntry(getBareJID(1), "gato11", new String[] { "Friends" }); roster.createEntry(getBareJID(2), "gato12", new String[] { "Family" }); - // Wait up to 2 seconds to receive new roster contacts - long initial = System.currentTimeMillis(); - while (System.currentTimeMillis() - initial < 2000 && roster.getEntryCount() != 2) { - Thread.sleep(100); - } + waitForCountdown(latch, roster, 2); - assertEquals("Wrong number of entries in connection 0", 2, roster.getEntryCount()); - - // Wait up to 2 seconds to receive presences of the new roster contacts - initial = System.currentTimeMillis(); - while (System.currentTimeMillis() - initial < 5000 && - (!roster.getPresence(getBareJID(1)).isAvailable() || - !roster.getPresence(getBareJID(2)).isAvailable())) - { - Thread.sleep(100); - } - assertTrue("Presence not received", roster.getPresence(getBareJID(1)).isAvailable()); - assertTrue("Presence not received", roster.getPresence(getBareJID(2)).isAvailable()); + CountDownLatch removeLatch = new CountDownLatch(2); + RosterListener latchCounter = new RemovalListener(removeLatch); + roster.addRosterListener(latchCounter); for (RosterEntry entry : roster.getEntries()) { roster.removeEntry(entry); - Thread.sleep(250); } - // Wait up to 2 seconds to receive roster removal notifications - initial = System.currentTimeMillis(); - while (System.currentTimeMillis() - initial < 2000 && roster.getEntryCount() != 0) { - Thread.sleep(100); - } + removeLatch.await(5, TimeUnit.SECONDS); + roster.removeRosterListener(latchCounter); assertEquals("Wrong number of entries in connection 0", 0, roster.getEntryCount()); assertEquals("Wrong number of groups in connection 0", 0, roster.getGroupCount()); - - assertEquals( - "Wrong number of entries in connection 1", - 0, - getConnection(1).getRoster().getEntryCount()); - assertEquals( - "Wrong number of groups in connection 1", - 0, - getConnection(1).getRoster().getGroupCount()); + assertEquals("Wrong number of entries in connection 1", 0, getConnection(1).getRoster().getEntryCount()); + assertEquals("Wrong number of groups in connection 1", 0, getConnection(1).getRoster().getGroupCount()); } /** @@ -174,41 +173,29 @@ public class RosterSmackTest extends SmackTestCase { try { // Add a new roster entry Roster roster = getConnection(0).getRoster(); + + CountDownLatch latch = new CountDownLatch(2); + setupCountdown(latch, roster); + roster.createEntry(getBareJID(1), "gato11", null); roster.createEntry(getBareJID(2), "gato12", null); - // Wait up to 2 seconds to let the server process presence subscriptions - long initial = System.currentTimeMillis(); - while (System.currentTimeMillis() - initial < 2000 && ( - !roster.getPresence(getBareJID(1)).isAvailable() || - !roster.getPresence(getBareJID(2)).isAvailable())) { - Thread.sleep(100); - } - - Thread.sleep(200); + waitForCountdown(latch, roster, 2); + CountDownLatch removeLatch = new CountDownLatch(2); + RosterListener latchCounter = new RemovalListener(removeLatch); + roster.addRosterListener(latchCounter); for (RosterEntry entry : roster.getEntries()) { roster.removeEntry(entry); - Thread.sleep(100); } - // Wait up to 2 seconds to receive roster removal notifications - initial = System.currentTimeMillis(); - while (System.currentTimeMillis() - initial < 2000 && roster.getEntryCount() != 0) { - Thread.sleep(100); - } + removeLatch.await(5, TimeUnit.SECONDS); + roster.removeRosterListener(latchCounter); assertEquals("Wrong number of entries in connection 0", 0, roster.getEntryCount()); assertEquals("Wrong number of groups in connection 0", 0, roster.getGroupCount()); - - assertEquals( - "Wrong number of entries in connection 1", - 0, - getConnection(1).getRoster().getEntryCount()); - assertEquals( - "Wrong number of groups in connection 1", - 0, - getConnection(1).getRoster().getGroupCount()); + assertEquals("Wrong number of entries in connection 1", 0, getConnection(1).getRoster().getEntryCount()); + assertEquals("Wrong number of groups in connection 1", 0, getConnection(1).getRoster().getGroupCount()); } catch (Exception e) { fail(e.getMessage()); @@ -226,15 +213,30 @@ public class RosterSmackTest extends SmackTestCase { try { // Add a new roster entry Roster roster = getConnection(0).getRoster(); + CountDownLatch latch = new CountDownLatch(1); + setupCountdown(latch, roster); + roster.createEntry(getBareJID(1), null, null); + + waitForCountdown(latch, roster, 1); - // Wait up to 2 seconds to let the server process presence subscriptions - long initial = System.currentTimeMillis(); - while (System.currentTimeMillis() - initial < 2000 && - !roster.getPresence(getBareJID(1)).isAvailable()) - { - Thread.sleep(100); - } + final CountDownLatch updateLatch = new CountDownLatch(2); + RosterListener latchCounter = new RosterListener() { + @Override + public void entriesAdded(Collection addresses) {} + + @Override + public void entriesUpdated(Collection addresses) { + updateLatch.countDown(); + } + + @Override + public void entriesDeleted(Collection addresses) {} + + @Override + public void presenceChanged(Presence presence) {} + }; + roster.addRosterListener(latchCounter); // Change the roster entry name and check if the change was made for (RosterEntry entry : roster.getEntries()) { @@ -243,7 +245,9 @@ public class RosterSmackTest extends SmackTestCase { } // Reload the roster and check the name again roster.reload(); - Thread.sleep(2000); + + updateLatch.await(5, TimeUnit.SECONDS); + for (RosterEntry entry : roster.getEntries()) { assertEquals("gato11", entry.getName()); } @@ -251,9 +255,6 @@ public class RosterSmackTest extends SmackTestCase { catch (Exception e) { fail(e.getMessage()); } - finally { - cleanUpRoster(); - } } /** @@ -303,9 +304,6 @@ public class RosterSmackTest extends SmackTestCase { } catch (Exception e) { fail(e.getMessage()); } - finally { - cleanUpRoster(); - } } /** @@ -361,9 +359,6 @@ public class RosterSmackTest extends SmackTestCase { catch (Exception e) { fail(e.getMessage()); } - finally { - cleanUpRoster(); - } } /** @@ -426,9 +421,6 @@ public class RosterSmackTest extends SmackTestCase { catch (Exception e) { fail(e.getMessage()); } - finally { - cleanUpRoster(); - } } /** @@ -441,75 +433,67 @@ public class RosterSmackTest extends SmackTestCase { * 5. Check that presence for each connected resource is correct */ public void testRosterPresences() throws Exception { - Thread.sleep(200); - try { - Presence presence; + Presence presence; - // Create another connection for the same user of connection 1 - ConnectionConfiguration connectionConfiguration = - new ConnectionConfiguration(getHost(), getPort(), getServiceName()); - XMPPConnection conn4 = new XMPPConnection(connectionConfiguration); - conn4.connect(); - conn4.login(getUsername(1), getPassword(1), "Home"); + // Create another connection for the same user of connection 1 + ConnectionConfiguration connectionConfiguration = + new ConnectionConfiguration(getHost(), getPort(), getServiceName()); + XMPPConnection conn4 = new XMPPConnection(connectionConfiguration); + conn4.connect(); + conn4.login(getUsername(1), getPassword(1), "Home"); - // Add a new roster entry - Roster roster = getConnection(0).getRoster(); - roster.createEntry(getBareJID(1), "gato11", null); + // Add a new roster entry + Roster roster = getConnection(0).getRoster(); + roster.createEntry(getBareJID(1), "gato11", null); - // Wait up to 2 seconds - long initial = System.currentTimeMillis(); - while (System.currentTimeMillis() - initial < 2000 && - (roster.getPresence(getBareJID(1)).getType() == Presence.Type.unavailable)) { - Thread.sleep(100); - } - - // Check that a presence is returned for a user - presence = roster.getPresence(getBareJID(1)); - assertTrue("Returned a null Presence for an existing user", presence.isAvailable()); - - // Check that the right presence is returned for a user+resource - presence = roster.getPresenceResource(getUsername(1) + "@" + conn4.getServiceName() + "/Home"); - assertEquals("Returned the wrong Presence", "Home", - StringUtils.parseResource(presence.getFrom())); - - // Check that the right presence is returned for a user+resource - presence = roster.getPresenceResource(getFullJID(1)); - assertTrue("Presence not found for user " + getFullJID(1), presence.isAvailable()); - assertEquals("Returned the wrong Presence", "Smack", - StringUtils.parseResource(presence.getFrom())); - - // Check the returned presence for a non-existent user+resource - presence = roster.getPresenceResource("noname@" + getServiceName() + "/Smack"); - assertFalse("Available presence was returned for a non-existing user", presence.isAvailable()); - assertEquals("Returned Presence for a non-existing user has the incorrect type", - Presence.Type.unavailable, presence.getType()); - - // Check that the returned presences are correct - Iterator presences = roster.getPresences(getBareJID(1)); - int count = 0; - while (presences.hasNext()) { - count++; - presences.next(); - } - assertEquals("Wrong number of returned presences", count, 2); - - // Close the connection so one presence must go - conn4.disconnect(); - - // Check that the returned presences are correct - presences = roster.getPresences(getBareJID(1)); - count = 0; - while (presences.hasNext()) { - count++; - presences.next(); - } - assertEquals("Wrong number of returned presences", count, 1); - - Thread.sleep(200); + // Wait up to 2 seconds + long initial = System.currentTimeMillis(); + while (System.currentTimeMillis() - initial < 2000 && + (roster.getPresence(getBareJID(1)).getType() == Presence.Type.unavailable)) { + Thread.sleep(100); } - finally { - cleanUpRoster(); + + // Check that a presence is returned for a user + presence = roster.getPresence(getBareJID(1)); + assertTrue("Returned a null Presence for an existing user", presence.isAvailable()); + + // Check that the right presence is returned for a user+resource + presence = roster.getPresenceResource(getUsername(1) + "@" + conn4.getServiceName() + "/Home"); + assertEquals("Returned the wrong Presence", "Home", + StringUtils.parseResource(presence.getFrom())); + + // Check that the right presence is returned for a user+resource + presence = roster.getPresenceResource(getFullJID(1)); + assertTrue("Presence not found for user " + getFullJID(1), presence.isAvailable()); + assertEquals("Returned the wrong Presence", "Smack", + StringUtils.parseResource(presence.getFrom())); + + // Check the returned presence for a non-existent user+resource + presence = roster.getPresenceResource("noname@" + getServiceName() + "/Smack"); + assertFalse("Available presence was returned for a non-existing user", presence.isAvailable()); + assertEquals("Returned Presence for a non-existing user has the incorrect type", + Presence.Type.unavailable, presence.getType()); + + // Check that the returned presences are correct + Iterator presences = roster.getPresences(getBareJID(1)); + int count = 0; + while (presences.hasNext()) { + count++; + presences.next(); } + assertEquals("Wrong number of returned presences", count, 2); + + // Close the connection so one presence must go + conn4.disconnect(); + + // Check that the returned presences are correct + presences = roster.getPresences(getBareJID(1)); + count = 0; + while (presences.hasNext()) { + count++; + presences.next(); + } + assertEquals("Wrong number of returned presences", count, 1); } /** @@ -605,6 +589,24 @@ public class RosterSmackTest extends SmackTestCase { for (int i=0; i addresses) {} + + @Override + public void entriesDeleted(Collection addresses) { + removalLatch.countDown(); + } + + @Override + public void entriesAdded(Collection addresses) {} + }); + for (RosterEntry entry : roster.getEntries()) { try { roster.removeEntry(entry); @@ -616,49 +618,16 @@ public class RosterSmackTest extends SmackTestCase { } try { - Thread.sleep(700); + removalLatch.await(5, TimeUnit.SECONDS); } catch (InterruptedException e) { fail(e.getMessage()); } } - // Wait up to 6 seconds to receive roster removal notifications - long initial = System.currentTimeMillis(); - while (System.currentTimeMillis() - initial < 6000 && ( - getConnection(0).getRoster().getEntryCount() != 0 || - getConnection(1).getRoster().getEntryCount() != 0 || - getConnection(2).getRoster().getEntryCount() != 0)) { - try { - Thread.sleep(100); - } catch (InterruptedException e) {} - } - assertEquals( - "Wrong number of entries in connection 0", - 0, - getConnection(0).getRoster().getEntryCount()); - assertEquals( - "Wrong number of groups in connection 0", - 0, - getConnection(0).getRoster().getGroupCount()); - - assertEquals( - "Wrong number of entries in connection 1", - 0, - getConnection(1).getRoster().getEntryCount()); - assertEquals( - "Wrong number of groups in connection 1", - 0, - getConnection(1).getRoster().getGroupCount()); - - assertEquals( - "Wrong number of entries in connection 2", - 0, - getConnection(2).getRoster().getEntryCount()); - assertEquals( - "Wrong number of groups in connection 2", - 0, - getConnection(2).getRoster().getGroupCount()); + assertEquals("Wrong number of entries in connection 0", 0, getConnection(0).getRoster().getEntryCount()); + assertEquals("Wrong number of entries in connection 1", 0, getConnection(1).getRoster().getEntryCount()); + assertEquals("Wrong number of entries in connection 2", 0, getConnection(2).getRoster().getEntryCount()); } /** @@ -708,15 +677,37 @@ public class RosterSmackTest extends SmackTestCase { } protected void setUp() throws Exception { - //XMPPConnection.DEBUG_ENABLED = false; - - try { - Thread.sleep(500); - } - catch (InterruptedException e) { - fail(e.getMessage()); - } - super.setUp(); + cleanUpRoster(); } + + + @Override + protected void tearDown() throws Exception { + cleanUpRoster(); + super.tearDown(); + } + + private class RemovalListener implements RosterListener { + private CountDownLatch latch; + + private RemovalListener(CountDownLatch removalLatch) { + latch = removalLatch; + } + + @Override + public void presenceChanged(Presence presence) {} + + @Override + public void entriesUpdated(Collection addresses) {} + + @Override + public void entriesDeleted(Collection addresses) { + latch.countDown(); + } + + @Override + public void entriesAdded(Collection addresses) {} + }; + } \ No newline at end of file From 3c975426f631310d2046dcb0aaf043bec992bdff Mon Sep 17 00:00:00 2001 From: rcollier Date: Mon, 22 Apr 2013 01:07:30 +0000 Subject: [PATCH 36/41] SMACK-433 Removed unnecessary reset of timeout. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13622 b35dd754-fafc-0310-a699-88a17e54d16e --- test/org/jivesoftware/smackx/entitycaps/EntityCapsTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/org/jivesoftware/smackx/entitycaps/EntityCapsTest.java b/test/org/jivesoftware/smackx/entitycaps/EntityCapsTest.java index 49a81f3e4..0725a6abf 100644 --- a/test/org/jivesoftware/smackx/entitycaps/EntityCapsTest.java +++ b/test/org/jivesoftware/smackx/entitycaps/EntityCapsTest.java @@ -35,10 +35,10 @@ public class EntityCapsTest extends SmackTestCase { return 2; } + @Override protected void setUp() throws Exception { super.setUp(); SmackConfiguration.setAutoEnableEntityCaps(true); - SmackConfiguration.setPacketReplyTimeout(1000 * 60 * 5); con0 = getConnection(0); con1 = getConnection(1); ecm0 = EntityCapsManager.getInstanceFor(getConnection(0)); @@ -47,7 +47,7 @@ public class EntityCapsTest extends SmackTestCase { sdm1 = ServiceDiscoveryManager.getInstanceFor(con1); letsAllBeFriends(); } - + public void testLocalEntityCaps() throws InterruptedException { DiscoverInfo info = EntityCapsManager.getDiscoveryInfoByNodeVer(ecm1.getLocalNodeVer()); assertFalse(info.containsFeature(DISCOVER_TEST_FEATURE)); From f0d729ef52b6d0fd9a378f031fb0a3f749d6b9d9 Mon Sep 17 00:00:00 2001 From: rcollier Date: Sat, 27 Apr 2013 15:37:21 +0000 Subject: [PATCH 37/41] SMACK-415 Added fix for handling the empty tag case. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13624 b35dd754-fafc-0310-a699-88a17e54d16e --- .../smackx/pubsub/provider/ItemProvider.java | 14 +- .../smackx/pubsub/ItemValidationTest.java | 208 ++++++++++++++---- 2 files changed, 168 insertions(+), 54 deletions(-) diff --git a/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java b/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java index 833392a43..05c2513f3 100644 --- a/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java +++ b/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java @@ -36,14 +36,13 @@ 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); + return new Item(id); } else { @@ -53,6 +52,7 @@ public class ItemProvider implements PacketExtensionProvider if (ProviderManager.getInstance().getExtensionProvider(payloadElemName, payloadNS) == null) { boolean done = false; + boolean isEmptyElement = false; StringBuilder payloadText = new StringBuilder(); while (!done) @@ -76,7 +76,7 @@ public class ItemProvider implements PacketExtensionProvider if (parser.isEmptyElementTag()) { payloadText.append("/>"); - done = true; + isEmptyElement = true; } else { @@ -85,8 +85,8 @@ public class ItemProvider implements PacketExtensionProvider } else if (parser.getEventType() == XmlPullParser.END_TAG) { - if (done) - done = false; + if (isEmptyElement) + isEmptyElement = false; else payloadText.append(""); } @@ -96,10 +96,10 @@ public class ItemProvider implements PacketExtensionProvider } tag = parser.next(); } - return new PayloadItem(id, node, new SimplePayload(payloadElemName, payloadNS, payloadText.toString())); + return new PayloadItem(id, new SimplePayload(payloadElemName, payloadNS, payloadText.toString())); } else { - return new PayloadItem(id, node, PacketParserUtils.parsePacketExtension( + return new PayloadItem(id, PacketParserUtils.parsePacketExtension( payloadElemName, payloadNS, parser)); } } diff --git a/test-unit/org/jivesoftware/smackx/pubsub/ItemValidationTest.java b/test-unit/org/jivesoftware/smackx/pubsub/ItemValidationTest.java index aee47a946..eb967e5f7 100644 --- a/test-unit/org/jivesoftware/smackx/pubsub/ItemValidationTest.java +++ b/test-unit/org/jivesoftware/smackx/pubsub/ItemValidationTest.java @@ -19,20 +19,25 @@ */ package org.jivesoftware.smackx.pubsub; -import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -import java.io.Reader; -import java.io.StringReader; - -import org.jivesoftware.smack.ThreadedDummyConnection; -import org.jivesoftware.smackx.pubsub.provider.ItemsProvider; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.xmlpull.mxp1.MXParser; -import org.xmlpull.v1.XmlPullParser; +import static org.custommonkey.xmlunit.XMLAssert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.Reader; +import java.io.StringReader; + +import org.jivesoftware.smack.TestUtils; +import org.jivesoftware.smack.ThreadedDummyConnection; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace; +import org.jivesoftware.smackx.pubsub.provider.ItemsProvider; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.xmlpull.mxp1.MXParser; +import org.xmlpull.v1.XmlPullParser; /** * @@ -95,37 +100,146 @@ public class ItemValidationTest assertXMLEqual(nodeIdCtrl, itemWithNodeId.toXML()); } -// @Test -// public void parseBasicItemWithoutNode() throws Exception -// { -// XmlPullParser parser = new MXParser(); -// Reader reader = new StringReader( -// "" + -// "" + -// "" + -// ""); -// parser.setInput(reader); -// ItemsProvider itemsProvider = new ItemsProvider(); -// ItemsExtension ext = (ItemsExtension) itemsProvider.parseExtension(parser); -// Item basicItem = (Item) ext.getItems().get(0); -// -// assertEquals("testid1", basicItem.getId()); -// assertNull(basicItem.getNode()); -// } - -// @Test -// public void parseBasicItemNode() throws Exception -// { -// BlockingQueue itemQ = new ArrayBlockingQueue(1); -// -// setupListener(itemQ); -// Message itemMsg = getMessage(""); -// connection.addMessage(itemMsg); -// -// Item basicItem = itemQ.poll(2, TimeUnit.SECONDS); -// -// assertNotNull(basicItem); -// assertEquals("testid1", basicItem.getId()); -// assertEquals("testNode", basicItem.getNode()); -// } + @Test + public void parseBasicItem() throws Exception + { + XmlPullParser parser = TestUtils.getMessageParser( + "" + + "" + + "" + + "" + + "" + + "" + + ""); + + Packet message = PacketParserUtils.parseMessage(parser); + PacketExtension eventExt = message.getExtension(PubSubNamespace.EVENT.getXmlns()); + + assertTrue(eventExt instanceof EventElement); + EventElement event = (EventElement) eventExt; + assertEquals(EventElementType.items, event.getEventType()); + assertEquals(1, event.getExtensions().size()); + assertTrue(event.getExtensions().get(0) instanceof ItemsExtension); + assertEquals(1, ((ItemsExtension)event.getExtensions().get(0)).items.size()); + + PacketExtension itemExt = ((ItemsExtension)event.getExtensions().get(0)).items.get(0); + assertTrue(itemExt instanceof Item); + assertEquals("testid1", ((Item)itemExt).getId()); + } + + @Test + public void parseSimplePayloadItem() throws Exception + { + String itemContent = "Some text"; + + XmlPullParser parser = TestUtils.getMessageParser( + "" + + "" + + "" + + "" + + itemContent + + "" + + "" + + "" + + ""); + + Packet message = PacketParserUtils.parseMessage(parser); + PacketExtension eventExt = message.getExtension(PubSubNamespace.EVENT.getXmlns()); + EventElement event = (EventElement) eventExt; + PacketExtension itemExt = ((ItemsExtension)event.getExtensions().get(0)).items.get(0); + + assertTrue(itemExt instanceof PayloadItem); + PayloadItem item = (PayloadItem)itemExt; + + assertEquals("testid1", item.getId()); + assertTrue(item.getPayload() instanceof SimplePayload); + + SimplePayload payload = (SimplePayload) item.getPayload(); + assertEquals("foo", payload.getElementName()); + assertEquals("smack:test", payload.getNamespace()); + assertXMLEqual(itemContent, payload.toXML()); + } + + @Test + public void parseComplexItem() throws Exception + { + String itemContent = + "" + + "Soliloquy" + + "

    " + + "To be, or not to be: that is the question:" + + "Whether 'tis nobler in the mind to suffer" + + "The slings and arrows of outrageous fortune," + + "Or to take arms against a sea of troubles," + + "And by opposing end them?" + + "" + + "" + + "tag:denmark.lit,2003:entry-32397" + + "2003-12-13T18:30:02Z" + + "2003-12-13T18:30:02Z" + + ""; + + XmlPullParser parser = TestUtils.getMessageParser( + "" + + "" + + "" + + "" + + itemContent + + "" + + "" + + "" + + ""); + + Packet message = PacketParserUtils.parseMessage(parser); + PacketExtension eventExt = message.getExtension(PubSubNamespace.EVENT.getXmlns()); + EventElement event = (EventElement) eventExt; + PacketExtension itemExt = ((ItemsExtension)event.getExtensions().get(0)).items.get(0); + + assertTrue(itemExt instanceof PayloadItem); + PayloadItem item = (PayloadItem)itemExt; + + assertEquals("testid1", item.getId()); + assertTrue(item.getPayload() instanceof SimplePayload); + + SimplePayload payload = (SimplePayload) item.getPayload(); + assertEquals("entry", payload.getElementName()); + assertEquals("http://www.w3.org/2005/Atom", payload.getNamespace()); + assertXMLEqual(itemContent, payload.toXML()); + } + + @Test + public void parseEmptyTag() throws Exception + { + String itemContent = ""; + + XmlPullParser parser = TestUtils.getMessageParser( + "" + + "" + + "" + + "" + + itemContent + + "" + + "" + + "" + + ""); + + Packet message = PacketParserUtils.parseMessage(parser); + PacketExtension eventExt = message.getExtension(PubSubNamespace.EVENT.getXmlns()); + + assertTrue(eventExt instanceof EventElement); + EventElement event = (EventElement) eventExt; + assertEquals(EventElementType.items, event.getEventType()); + assertEquals(1, event.getExtensions().size()); + assertTrue(event.getExtensions().get(0) instanceof ItemsExtension); + assertEquals(1, ((ItemsExtension)event.getExtensions().get(0)).items.size()); + + PacketExtension itemExt = ((ItemsExtension)event.getExtensions().get(0)).items.get(0); + assertTrue(itemExt instanceof PayloadItem); + PayloadItem item = (PayloadItem)itemExt; + + assertEquals("testid1", item.getId()); + assertTrue(item.getPayload() instanceof SimplePayload); + + assertXMLEqual(itemContent, ((SimplePayload)item.getPayload()).toXML()); + } } From 238ce7aac303de55dc4b0cab792395e8630f62c4 Mon Sep 17 00:00:00 2001 From: rcollier Date: Mon, 29 Apr 2013 11:40:49 +0000 Subject: [PATCH 38/41] SMACK-415 Added code that was mistakenly removed to handle optional node attribute git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13625 b35dd754-fafc-0310-a699-88a17e54d16e --- .../jivesoftware/smackx/pubsub/provider/ItemProvider.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java b/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java index 05c2513f3..314d7c921 100644 --- a/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java +++ b/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java @@ -36,13 +36,14 @@ 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); + return new Item(id, node); } else { @@ -96,10 +97,10 @@ public class ItemProvider implements PacketExtensionProvider } tag = parser.next(); } - return new PayloadItem(id, new SimplePayload(payloadElemName, payloadNS, payloadText.toString())); + return new PayloadItem(id, node, new SimplePayload(payloadElemName, payloadNS, payloadText.toString())); } else { - return new PayloadItem(id, PacketParserUtils.parsePacketExtension( + return new PayloadItem(id, node, PacketParserUtils.parsePacketExtension( payloadElemName, payloadNS, parser)); } } From 6b70974269553a4ae3ee260e26e3b0011a213109 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Fri, 3 May 2013 09:48:41 +0000 Subject: [PATCH 39/41] SMACK-437 Replaced 'String.isEmpty()' with 'String.length() > 0' git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13631 b35dd754-fafc-0310-a699-88a17e54d16e --- .../jivesoftware/smackx/pubsub/provider/ItemProvider.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java b/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java index 314d7c921..7b06af301 100644 --- a/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java +++ b/source/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java @@ -65,11 +65,11 @@ public class ItemProvider implements PacketExtensionProvider else if (parser.getEventType() == XmlPullParser.START_TAG) { payloadText.append("<").append(parser.getName()); - - if (parser.getName().equals(payloadElemName) && (!payloadNS.isEmpty())) + + if (parser.getName().equals(payloadElemName) && (payloadNS.length() > 0)) 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("\""); From 57051abfeac76d158edde36002e52d9f20878864 Mon Sep 17 00:00:00 2001 From: rcollier Date: Sat, 4 May 2013 12:15:17 +0000 Subject: [PATCH 40/41] Updated version information for 3.3 release. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13638 b35dd754-fafc-0310-a699-88a17e54d16e --- build/build.xml | 2 +- source/org/jivesoftware/smack/SmackConfiguration.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/build.xml b/build/build.xml index cb12e905b..1737c4b54 100644 --- a/build/build.xml +++ b/build/build.xml @@ -28,7 +28,7 @@ - + diff --git a/source/org/jivesoftware/smack/SmackConfiguration.java b/source/org/jivesoftware/smack/SmackConfiguration.java index fcb1d4beb..aae8184bc 100644 --- a/source/org/jivesoftware/smack/SmackConfiguration.java +++ b/source/org/jivesoftware/smack/SmackConfiguration.java @@ -48,7 +48,7 @@ import org.xmlpull.v1.XmlPullParser; */ public final class SmackConfiguration { - private static final String SMACK_VERSION = "3.2.2"; + private static final String SMACK_VERSION = "3.3.0"; private static int packetReplyTimeout = 5000; private static int keepAliveInterval = 30000; From df022d155a663b6624f978a8b54ac7aa0ecec9be Mon Sep 17 00:00:00 2001 From: rcollier Date: Sat, 4 May 2013 13:03:10 +0000 Subject: [PATCH 41/41] Updated changelog for 3.3 release. git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13640 b35dd754-fafc-0310-a699-88a17e54d16e --- build/resources/releasedocs/changelog.html | 67 +++++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/build/resources/releasedocs/changelog.html b/build/resources/releasedocs/changelog.html index 9d75fd6a1..c4f969394 100644 --- a/build/resources/releasedocs/changelog.html +++ b/build/resources/releasedocs/changelog.html @@ -141,9 +141,61 @@ hr {
    +

    3.3.0 -- May 4, 2013

    + +

    Bug Fixes

    +
      +
    • [SMACK-225] - Improper handeling of DNS SRV records
    • +
    • [SMACK-238] - The vCard avatar type always return jpg
    • +
    • [SMACK-270] - Fix for a memory leak in MUC with MUC.finalize()
    • +
    • [SMACK-278] - Deadlock during Smack disconnect
    • +
    • [SMACK-342] - VCards causes ConcurrentModificationException
    • +
    • [SMACK-344] - Bug in SASL authentication mechanism when SRV records are being used.
    • +
    • [SMACK-351] - Rework File Transfer
    • +
    • [SMACK-352] - Update the licensing headers in various files.
    • +
    • [SMACK-355] - IO Error if smack cant use port for local proxy
    • +
    • [SMACK-371] - Some MUC tasks are using stanza's as defined in an older version of the spec. Fails to work on some servers.
    • +
    • [SMACK-375] - Node strings in the discovery info packets are not escaped as in the other packets
    • +
    • [SMACK-382] - Prevent memory leak in AdHocCommandManager
    • +
    • [SMACK-384] - Endless waiting for connection to be established
    • +
    • [SMACK-390] - Smack login will fail if a bad delay packet is received
    • +
    • [SMACK-392] - In ant build, compile-test target doesn't work.
    • +
    • [SMACK-394] - Erroneous cast in IBBInputStream's read() method
    • +
    • [SMACK-395] - Socks5BytestreamManager's establishConnection() should still try to use the local streamhost proxy if the server doesn't provide one
    • +
    • [SMACK-404] - Smack uses the wrong method to decode Base64 Strings
    • +
    • [SMACK-413] - VCardProvider incorrectly parses binary value of avatars
    • +
    • [SMACK-415] - ItemProvider relies on incorrect behavior of MXParser, violating the contract of the XMLPullParser interface
    • +
    • [SMACK-417] - If both PacketReader and PacketWriter fail at the same time, connectionClosedonError() is called two times
    • +
    + +

    New Features

    +
      +
    • [SMACK-331] - Add support for XEP-0184: Message Delivery Receipts
    • +
    • [SMACK-345] - Inproved detection of last activity
    • +
    • [SMACK-361] - Add support for XEP-0115 Entity Capabilities
    • +
    • [SMACK-376] - Setting a custom trust manager to control certificates from outside
    • +
    • [SMACK-388] - XEP-199 XMPP Ping support
    • +
    + +

    Improvements

    +
      +
    • [SMACK-341] - Update the PacketCollector and ConnectionDetachedPacketCollector to use the java concurrent classes.
    • +
    • [SMACK-358] - Support additional properties for account creation in test cases.
    • +
    • [SMACK-363] - Code Cleanup
    • +
    • [SMACK-377] - avoid unnecessary DNS requests in XMPPconnection
    • +
    • [SMACK-379] - Sessions were removed from the specification but Smack still uses them. Should be updated to reflect the spec changes.
    • +
    • [SMACK-385] - Reusing KeyStore in order to reduce memory usage
    • +
    • [SMACK-389] - Add java.util.zip.Deflater(In|Out)putStream as Java7 API native alternative to JZlib
    • +
    • [SMACK-391] - Improve date parsing in StringUtils and make DelayInformationProvider use StringUtils for date parsing.
    • +
    • [SMACK-412] - Replace the whitespace ping with a XEP-0199 ping
    • +
    • [SMACK-419] - PacketWriter: Only flush the BufferedWriter if the packet queue is empty
    • +
    • [SMACK-423] - Investigate whether unhandled packets should still parse the child xml into a string as content
    • +
    • [SMACK-430] - Throw an exception if FileTransferManager.createOutgoingFileTransfer() was used with a bare JID
    • +
    +

    3.2.2 -- Dec. 23, 2011

    -

    Bug

    +

    Bug Fixes

    • [SMACK-263] - Set file info in all send* methods
    • [SMACK-322] - NPE in XMPPConnection
    • @@ -157,15 +209,14 @@ hr {
    • [SMACK-362] - smack throw NoSuchElementException if the muc#roominfo_subject has no values
    -

    Improvement

    +

    Improvements

    • [SMACK-343] - Make Smack jar an OSGi bundle.
    • [SMACK-354] - Provide milliseconds in timestamp colum debugwindow
    -

    3.2.1 -- July 4, 2011

    -

    Bug

    +

    Bug Fixes

    • [SMACK-129] - MultiUserChat will Store Messages in its PacketCollector irregardless of whether or not they are being read
    • [SMACK-230] - Disconnect Can Cause Null Pointer Exception
    • @@ -176,14 +227,14 @@ hr {

    3.2.0 -- May 3, 2011

    -

    New Feature

    +

    New Features

    • [SMACK-272] - Add support for pubsub (XEP-0060)
    • [SMACK-296] - Add support for XEP-0224: Attention
    • [SMACK-319] - Add common interfaces for SOCKS5 Bytestreams and In-Band Bytestreams
    -

    Improvement

    +

    Improvements

    • [SMACK-137] - File Transfer Settings
    • [SMACK-156] - Add the ability to register for roster events before logging in
    • @@ -208,9 +259,9 @@ hr {
    • [SMACK-310] - Add Support for Localized Message Subjects
    -

    Bug

    +

    Bug Fixes

      -
    • [SMACK-163] - Fix NPE in RoomInfo when subject has not value
    • +
    • [SMACK-163] - Fix NPE in RoomInfo when subject has not value
    • [SMACK-207] - Parsing of messages may disconnect Smack/Spark
    • [SMACK-225] - Improper handeling of DNS SRV records
    • [SMACK-232] - Better handling of Roster error