diff --git a/build/eclipse/classpath b/build/eclipse/classpath
index 4238868be..ddf552712 100644
--- a/build/eclipse/classpath
+++ b/build/eclipse/classpath
@@ -11,7 +11,6 @@
-
@@ -28,5 +27,6 @@
+
diff --git a/source/org/jivesoftware/smack/Connection.java b/source/org/jivesoftware/smack/Connection.java
index cde0ab4aa..d041067a4 100644
--- a/source/org/jivesoftware/smack/Connection.java
+++ b/source/org/jivesoftware/smack/Connection.java
@@ -23,8 +23,10 @@ package org.jivesoftware.smack;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Constructor;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -33,6 +35,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
+import org.jivesoftware.smack.compression.JzlibInputOutputStream;
+import org.jivesoftware.smack.compression.XMPPInputOutputStream;
+import org.jivesoftware.smack.compression.Java7ZlibInputOutputStream;
import org.jivesoftware.smack.debugger.SmackDebugger;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.packet.Packet;
@@ -90,6 +95,8 @@ public abstract class Connection {
private final static Set connectionEstablishedListeners =
new CopyOnWriteArraySet();
+ protected final static List compressionHandlers = new ArrayList(2);
+
/**
* Value that indicates whether debugging is enabled. When enabled, a debug
* window will apear for each new connection that will contain the following
@@ -116,6 +123,10 @@ public abstract class Connection {
}
// Ensure the SmackConfiguration class is loaded by calling a method in it.
SmackConfiguration.getVersion();
+ // Add the Java7 compression handler first, since it's preferred
+ compressionHandlers.add(new Java7ZlibInputOutputStream());
+ // If we don't have access to the Java7 API use the JZlib compression handler
+ compressionHandlers.add(new JzlibInputOutputStream());
}
/**
@@ -193,6 +204,8 @@ public abstract class Connection {
*/
protected final ConnectionConfiguration config;
+ protected XMPPInputOutputStream compressionHandler;
+
/**
* Create a new Connection to a XMPP server.
*
diff --git a/source/org/jivesoftware/smack/XMPPConnection.java b/source/org/jivesoftware/smack/XMPPConnection.java
index 86aa5ad74..4024bd340 100644
--- a/source/org/jivesoftware/smack/XMPPConnection.java
+++ b/source/org/jivesoftware/smack/XMPPConnection.java
@@ -20,6 +20,7 @@
package org.jivesoftware.smack;
+import org.jivesoftware.smack.compression.XMPPInputOutputStream;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
@@ -33,9 +34,17 @@ import javax.net.ssl.SSLSocket;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;
-import java.io.*;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyStore;
@@ -85,10 +94,11 @@ public class XMPPConnection extends Connection {
* Collection of available stream compression methods offered by the server.
*/
private Collection compressionMethods;
+
/**
- * Flag that indicates if stream compression is actually in use.
+ * Set to true by packet writer if the server acknowledged the compression
*/
- private boolean usingCompression;
+ private boolean serverAckdCompression = false;
/**
@@ -571,7 +581,8 @@ public class XMPPConnection extends Connection {
private void initConnection() throws XMPPException {
boolean isFirstInitialization = packetReader == null || packetWriter == null;
if (!isFirstInitialization) {
- usingCompression = false;
+ compressionHandler = null;
+ serverAckdCompression = false;
}
// Set the reader and writer instance variables
@@ -669,7 +680,7 @@ public class XMPPConnection extends Connection {
private void initReaderAndWriter() throws XMPPException {
try {
- if (!usingCompression) {
+ if (compressionHandler == null) {
reader =
new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
writer = new BufferedWriter(
@@ -677,24 +688,15 @@ public class XMPPConnection extends Connection {
}
else {
try {
- Class> zoClass = Class.forName("com.jcraft.jzlib.ZOutputStream");
- Constructor> constructor =
- zoClass.getConstructor(OutputStream.class, Integer.TYPE);
- Object out = constructor.newInstance(socket.getOutputStream(), 9);
- Method method = zoClass.getMethod("setFlushMode", Integer.TYPE);
- method.invoke(out, 2);
- writer =
- new BufferedWriter(new OutputStreamWriter((OutputStream) out, "UTF-8"));
+ OutputStream os = compressionHandler.getOutputStream(socket.getOutputStream());
+ writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
- Class> ziClass = Class.forName("com.jcraft.jzlib.ZInputStream");
- constructor = ziClass.getConstructor(InputStream.class);
- Object in = constructor.newInstance(socket.getInputStream());
- method = ziClass.getMethod("setFlushMode", Integer.TYPE);
- method.invoke(in, 2);
- reader = new BufferedReader(new InputStreamReader((InputStream) in, "UTF-8"));
+ InputStream is = compressionHandler.getInputStream(socket.getInputStream());
+ reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
}
catch (Exception e) {
e.printStackTrace();
+ compressionHandler = null;
reader = new BufferedReader(
new InputStreamReader(socket.getInputStream(), "UTF-8"));
writer = new BufferedWriter(
@@ -869,17 +871,27 @@ public class XMPPConnection extends Connection {
}
/**
- * Returns true if the specified compression method was offered by the server.
- *
- * @param method the method to check.
- * @return true if the specified compression method was offered by the server.
+ * Returns the compression handler that can be used for one compression methods offered by the server.
+ *
+ * @return a instance of XMPPInputOutputStream or null if no suitable instance was found
+ *
*/
- private boolean hasAvailableCompressionMethod(String method) {
- return compressionMethods != null && compressionMethods.contains(method);
+ private XMPPInputOutputStream maybeGetCompressionHandler() {
+ if (compressionMethods != null) {
+ for (XMPPInputOutputStream handler : compressionHandlers) {
+ if (!handler.isSupported())
+ continue;
+
+ String method = handler.getCompressionMethod();
+ if (compressionMethods.contains(method))
+ return handler;
+ }
+ }
+ return null;
}
public boolean isUsingCompression() {
- return usingCompression;
+ return compressionHandler != null && serverAckdCompression;
}
/**
@@ -902,14 +914,9 @@ public class XMPPConnection extends Connection {
if (authenticated) {
throw new IllegalStateException("Compression should be negotiated before authentication.");
}
- try {
- Class.forName("com.jcraft.jzlib.ZOutputStream");
- }
- catch (ClassNotFoundException e) {
- throw new IllegalStateException("Cannot use compression. Add smackx.jar to the classpath");
- }
- if (hasAvailableCompressionMethod("zlib")) {
- requestStreamCompression();
+
+ if ((compressionHandler = maybeGetCompressionHandler()) != null) {
+ requestStreamCompression(compressionHandler.getCompressionMethod());
// Wait until compression is being used or a timeout happened
synchronized (this) {
try {
@@ -919,7 +926,7 @@ public class XMPPConnection extends Connection {
// Ignore.
}
}
- return usingCompression;
+ return isUsingCompression();
}
return false;
}
@@ -929,10 +936,10 @@ public class XMPPConnection extends Connection {
* then negotiation of stream compression can only happen after TLS was negotiated. If TLS
* compression is being used the stream compression should not be used.
*/
- private void requestStreamCompression() {
+ private void requestStreamCompression(String method) {
try {
writer.write("");
- writer.write("zlib");
+ writer.write("" + method + "");
writer.flush();
}
catch (IOException e) {
@@ -946,8 +953,7 @@ public class XMPPConnection extends Connection {
* @throws Exception if there is an exception starting stream compression.
*/
void startStreamCompression() throws Exception {
- // Secure the plain connection
- usingCompression = true;
+ serverAckdCompression = true;
// Initialize the reader and writer with the new secured version
initReaderAndWriter();
@@ -986,7 +992,7 @@ public class XMPPConnection extends Connection {
* appropiate error messages to end-users.
*/
public void connect() throws XMPPException {
- // Stablishes the connection, readers and writers
+ // Establishes the connection, readers and writers
connectUsingConfiguration(config);
// Automatically makes the login if the user was previouslly connected successfully
// to the server and the connection was terminated abruptly
diff --git a/source/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java b/source/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java
new file mode 100644
index 000000000..dc504517b
--- /dev/null
+++ b/source/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright 2013 Florian Schmaus
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smack.compression;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * This class provides XMPP "zlib" compression with the help of the Deflater class of the Java API. Note that the method
+ * needed is available since Java7, so it will only work with Java7 or higher (hence it's name).
+ *
+ * @author Florian Schmaus
+ * @see The
+ * required deflate() method
+ *
+ */
+public class Java7ZlibInputOutputStream extends XMPPInputOutputStream {
+ private final static Method method;
+ private final static boolean supported;
+ private final static int compressionLevel = Deflater.DEFAULT_STRATEGY;
+
+ static {
+ Method m = null;
+ try {
+ m = Deflater.class.getMethod("deflate", byte[].class, int.class, int.class, int.class);
+ } catch (SecurityException e) {
+ } catch (NoSuchMethodException e) {
+ }
+ method = m;
+ supported = (method != null);
+ }
+
+ public Java7ZlibInputOutputStream() {
+ compressionMethod = "zlib";
+ }
+
+ @Override
+ public boolean isSupported() {
+ return supported;
+ }
+
+ @Override
+ public InputStream getInputStream(InputStream inputStream) {
+ return new InflaterInputStream(inputStream, new Inflater(), 512) {
+ /**
+ * Provide a more InputStream compatible version. A return value of 1 means that it is likely to read one
+ * byte without blocking, 0 means that the system is known to block for more input.
+ *
+ * @return 0 if no data is available, 1 otherwise
+ * @throws IOException
+ */
+ @Override
+ public int available() throws IOException {
+ /*
+ * aSmack related remark (where KXmlParser is used):
+ * This is one of the funny code blocks. InflaterInputStream.available violates the contract of
+ * InputStream.available, which breaks kXML2.
+ *
+ * I'm not sure who's to blame, oracle/sun for a broken api or the google guys for mixing a sun bug with
+ * a xml reader that can't handle it....
+ *
+ * Anyway, this simple if breaks suns distorted reality, but helps to use the api as intended.
+ */
+ if (inf.needsInput()) {
+ return 0;
+ }
+ return super.available();
+ }
+ };
+ }
+
+ @Override
+ public OutputStream getOutputStream(OutputStream outputStream) {
+ return new DeflaterOutputStream(outputStream, new Deflater(compressionLevel)) {
+ public void flush() throws IOException {
+ if (!supported) {
+ super.flush();
+ return;
+ }
+ int count = 0;
+ if (!def.needsInput()) {
+ do {
+ count = def.deflate(buf, 0, buf.length);
+ out.write(buf, 0, count);
+ } while (count > 0);
+ out.flush();
+ }
+ try {
+ do {
+ count = (Integer) method.invoke(def, buf, 0, buf.length, 2);
+ out.write(buf, 0, count);
+ } while (count > 0);
+ } catch (IllegalArgumentException e) {
+ throw new IOException("Can't flush");
+ } catch (IllegalAccessException e) {
+ throw new IOException("Can't flush");
+ } catch (InvocationTargetException e) {
+ throw new IOException("Can't flush");
+ }
+ super.flush();
+ }
+ };
+ }
+
+}
diff --git a/source/org/jivesoftware/smack/compression/JzlibInputOutputStream.java b/source/org/jivesoftware/smack/compression/JzlibInputOutputStream.java
new file mode 100644
index 000000000..7db07739c
--- /dev/null
+++ b/source/org/jivesoftware/smack/compression/JzlibInputOutputStream.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2013 Florian Schmaus
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smack.compression;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * This class provides XMPP "zlib" compression with the help of JZLib. Note that jzlib-1.0.7 must be used (i.e. in the
+ * classpath), newer versions won't work!
+ *
+ * @author Florian Schmaus
+ * @see JZLib
+ *
+ */
+public class JzlibInputOutputStream extends XMPPInputOutputStream {
+
+ private static Class> zoClass = null;
+ private static Class> ziClass = null;
+
+ static {
+ try {
+ zoClass = Class.forName("com.jcraft.jzlib.ZOutputStream");
+ ziClass = Class.forName("com.jcraft.jzlib.ZInputStream");
+ } catch (ClassNotFoundException e) {
+ }
+ }
+
+ public JzlibInputOutputStream() {
+ compressionMethod = "zlib";
+ }
+
+ @Override
+ public boolean isSupported() {
+ return (zoClass != null && ziClass != null);
+ }
+
+ @Override
+ public InputStream getInputStream(InputStream inputStream) throws SecurityException, NoSuchMethodException,
+ IllegalArgumentException, IllegalAccessException, InvocationTargetException, InstantiationException {
+ Constructor> constructor = ziClass.getConstructor(InputStream.class);
+ Object in = constructor.newInstance(inputStream);
+
+ Method method = ziClass.getMethod("setFlushMode", Integer.TYPE);
+ method.invoke(in, 2);
+ return (InputStream) in;
+ }
+
+ @Override
+ public OutputStream getOutputStream(OutputStream outputStream) throws SecurityException, NoSuchMethodException,
+ IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
+ Constructor> constructor = zoClass.getConstructor(OutputStream.class, Integer.TYPE);
+ Object out = constructor.newInstance(outputStream, 9);
+
+ Method method = zoClass.getMethod("setFlushMode", Integer.TYPE);
+ method.invoke(out, 2);
+ return (OutputStream) out;
+ }
+}
diff --git a/source/org/jivesoftware/smack/compression/XMPPInputOutputStream.java b/source/org/jivesoftware/smack/compression/XMPPInputOutputStream.java
new file mode 100644
index 000000000..d44416a61
--- /dev/null
+++ b/source/org/jivesoftware/smack/compression/XMPPInputOutputStream.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2013 Florian Schmaus
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smack.compression;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public abstract class XMPPInputOutputStream {
+ protected String compressionMethod;
+
+ public String getCompressionMethod() {
+ return compressionMethod;
+ }
+
+ public abstract boolean isSupported();
+
+ public abstract InputStream getInputStream(InputStream inputStream) throws Exception;
+
+ public abstract OutputStream getOutputStream(OutputStream outputStream) throws Exception;
+}
diff --git a/test/config/test-case.example.xml b/test/config/test-case.example.xml
index a1e5529dc..f8dd075f0 100644
--- a/test/config/test-case.example.xml
+++ b/test/config/test-case.example.xml
@@ -26,4 +26,6 @@
-->
+ false
+
diff --git a/test/org/jivesoftware/smack/test/SmackTestCase.java b/test/org/jivesoftware/smack/test/SmackTestCase.java
index d28aee83f..044a60cf4 100644
--- a/test/org/jivesoftware/smack/test/SmackTestCase.java
+++ b/test/org/jivesoftware/smack/test/SmackTestCase.java
@@ -64,6 +64,7 @@ public abstract class SmackTestCase extends TestCase {
private Map accountCreationParameters = new HashMap();
private boolean samePassword;
private List createdUserIdx = new ArrayList();
+ private boolean compressionEnabled = false;
private String[] usernames;
private String[] passwords;
@@ -148,7 +149,7 @@ public abstract class SmackTestCase extends TestCase {
protected XMPPConnection createConnection() {
// Create the configuration for this new connection
ConnectionConfiguration config = new ConnectionConfiguration(host, port);
- config.setCompressionEnabled(Boolean.getBoolean("test.compressionEnabled"));
+ config.setCompressionEnabled(compressionEnabled);
config.setSendPresence(sendInitialPresence());
if (getSocketFactory() == null) {
config.setSocketFactory(getSocketFactory());
@@ -456,6 +457,9 @@ public abstract class SmackTestCase extends TestCase {
accountCreationParameters.put(key, value);
}
}
+ else if (parser.getName().equals("compressionEnabled")) {
+ compressionEnabled = "true".equals(parser.nextText());
+ }
}
eventType = parser.next();
}