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(); }