3
votes

I am working on an application involving two Smack clients, one of which attempts to send a jar file to the other client. I am writing this as a part of a desktop application, not an Android application (thought I'd make that distinction since most Smack queries on StackOverflow are Android-related). I am using Openfire 3.9.3 as the XMPP server. For all code examples I am using the following Smack libraries (version 4.1.0- beta 1): smack-java7, smack-tcp, smack-extensions, and smack-sasl-provided. Here's the code for the OutgoingFileTransfer (note in all code examples info such as Smack usernames have been anonymized):

// LOG is of type org.apache.logging.log4j.Logger
// transferManager is the FileTransferManager for this XMPPTCPConnection
private void sendJarFile(final String to)
{
    LOG.info("Sending " + jarFileName + " to " + to);
    OutgoingFileTransfer jarTransfer = 
            transferManager.createOutgoingFileTransfer(to);
    final File jarFile = new File(jarFileName);
    try
    {
        jarTransfer.sendFile(jarFile, "The current jar file");
        while (!jarTransfer.isDone())
        {
            LOG.info("File transfer status: " + jarTransfer.getStatus());
            Thread.sleep(500);
        }
        LOG.info("File transfer to " + to + " is done");
        // Now that the file transfer is done check for errors
        // or exceptions
        FileTransfer.Error error = jarTransfer.getError();
        if (error != null)
        {
            LOG.error(error.getMessage());
        }
        Exception exception = jarTransfer.getException();
        if (exception != null)
        {
            if (exception instanceof XMPPException.XMPPErrorException)
            {
                XMPPException.XMPPErrorException errorException = 
                        (XMPPException.XMPPErrorException)exception;
                XMPPError xmppError = errorException.getXMPPError();
                LOG.error(xmppError);
                LOG.error("Descriptive text: " + xmppError.getDescriptiveText());
                LOG.error("Condition: " + xmppError.getCondition());
                LOG.error("Type: " + xmppError.getType());
            }
        }
    }
    // For now, just catching and logging exceptions. Exception handling
    // will be added in top-level classes
    catch (SmackException e)
    {
        LOG.error("Exception trying to send jar file", e);
    }
    catch (InterruptedException e)
    {
        // Do nothing
    }
    catch (Exception e)
    {
        LOG.error("Exception trying to send jar file", e);
    }
}

The output from this code is:

Sending test.jar to receiver@local_openfire/Smack

14:09:43.748 INFO - File transfer status: Initial

14:09:44.249 INFO - File transfer status: Negotiating Stream

14:09:44.751 INFO - File transfer status: Negotiating Stream

// This message continues for several seconds, until finally

14:09:53.805 INFO - File transfer status: Negotiating Stream

14:09:54.308 INFO - File transfer to receiver@local_openfire/Smack is done

14:09:54.309 ERROR - org.jivesoftware.smack.packet.XMPPError@30e95075

14:09:54.310 ERROR - Descriptive text: null

14:09:54.310 ERROR - Condition: service-unavailable

14:09:54.310 ERROR - Type: cancel

For the IncomingFileTransfer, the code is:

@Override
public void fileTransferRequest(FileTransferRequest request)
{
    final String requestorId = request.getRequestor();
    LOG.info("FileTransferRequest from: " + requestorId);
    // Only respond to requests from the sender
    if (requestorId.contains(senderId))
    {
        final IncomingFileTransfer transfer = request.accept();
        LOG.info("FileTransferRequest accepted");
        try
        {
            final String fileName = transfer.getFileName();
            transfer.recieveFile(new File(fileName));
            LOG.info("Incoming file transfer: " + fileName);
            LOG.info("Transfer status is: " + transfer.getStatus());
            while (!transfer.isDone())
            {
                final double progress = transfer.getProgress();
                final double progressPercent = progress * 100.0;
                String percComplete = String.format("%1$,.2f", progressPercent);
                LOG.info("Transfer status is: " + transfer.getStatus());
                LOG.info("File transfer is " + percComplete + "% complete");
                Thread.sleep(1000);
            }

            // Now that the file transfer is done check for errors
            // or exceptions
            FileTransfer.Error transferError = transfer.getError();
            if (transferError != null)
            {
                LOG.error("Transfer error occurred: " + transferError.getMessage());
            }
            Exception transferException = transfer.getException();
            if (transferException != null)
            {
                LOG.error("Transfer exception occurred: " + transferException);
                if (transferException instanceof SmackException.NoResponseException)
                {
                    SmackException.NoResponseException smackException = (SmackException.NoResponseException)transferException;
                    smackException.printStackTrace();
                }
            }
            LOG.info("FileTransfer complete");
            provisioningComplete = true;
        }
        // For now just logging exceptions
        catch (SmackException e)
        {
            LOG.error("SmackException trying to receive jar file", e);
        }
        catch (InterruptedException e)
        {
            // Do nothing
        }
        catch (IOException e)
        {
            LOG.error("IOException trying to receive jar file", e);
        }

    }
    else
    {
        LOG.warn("FileTransferRequest rejected");
        try
        {
            request.reject();
        }
        catch (NotConnectedException e)
        {
            LOG.warn("NotConnectedException when rejecting FileTransferRequest");
        }
    }
}

The output from this code is:

14:09:43.766 INFO - FileTransferRequest from: sender@local_openfire/Smack

14:09:43.767 INFO - FileTransferRequest accepted

14:09:43.768 INFO - Incoming file transfer: test.jar

14:09:43.769 INFO - Transfer status is: Negotiating Transfer

14:09:43.770 INFO - Transfer status is: Negotiating Stream

14:09:43.770 INFO - File transfer is 0.00% complete

14:09:44.771 INFO - Transfer status is: Negotiating Stream

14:09:44.771 INFO - File transfer is 0.00% complete

14:09:45.776 INFO - Transfer status is: Negotiating Stream

14:09:45.776 INFO - File transfer is 0.00% complete

14:09:46.778 INFO - Transfer status is: Negotiating Stream

14:09:46.778 INFO - File transfer is 0.00% complete

14:09:47.782 INFO - Transfer status is: Negotiating Stream

14:09:47.783 INFO - File transfer is 0.00% complete

14:09:48.784 ERROR - Transfer exception occurred: org.jivesoftware.smack.SmackException: Error in execution

14:09:48.784 INFO - FileTransfer complete

After this code has run, on the receiver end, I have a file named "test.jar" in the current working directory with a file size of 0 bytes. I've tried this both with the sender and receiver on different machines, and the sender and receiver on the same machine. I was initially using Smack 4.0.6, but switched to the latest code base (4.1.0- beta 1 as of this writing) in hopes that perhaps this bug would have been resolved. No such luck. I would appreciate any suggestions. Thanks!

UPDATE 1-30-2015

I am no longer seeing the XMPPError on the sender side. Instead, the sender remains stuck at the File transfer status: Negotiating Stream state. However, the receiver gets the following error:

SmackException.NoResponseException: No response received within packet reply timed out. Timeout was 5000ms (~5s)

I can see how to up the timeout for the OutgoingFileTransfer class, but not for IncomingFileTransfer.

UPDATE 2-02-2015

I used the Smack debugging tool and captured the raw XML stanzas. For the sake of brevity, I only included those that were relevant to file transfer (i.e, not presence or roster packets). Here they are:

<iq to="receiver@smack_server/Smack" id="NK8Lh-11" type="set" 
from="sender@smack_server/Smack">
    <si xmlns="http://jabber.org/protocol/si" id="jsi_3077759398544954943" 
    mime-type="text/plain" profile="http://jabber.org/protocol/si/profile/file-transfer">
        <file xmlns="http://jabber.org/protocol/si/profile/file-transfer" name="test.txt" 
        size="37">
            <desc>A test file</desc>
        </file>
        <feature xmlns="http://jabber.org/protocol/feature-neg">
            <x xmlns="jabber:x:data" type="form">
                <field var="stream-method" type="list-single">
                    <option>
                        <value>http://jabber.org/protocol/bytestreams</value>
                    </option>
                    <option>
                        <value>http://jabber.org/protocol/ibb</value>
                    </option>
                </field>
            </x>
        </feature>
    </si>
</iq>

<iq to="sender@smack_server/Smack" id="NK8Lh-11" type="result">
  <si xmlns="http://jabber.org/protocol/si">
    <feature xmlns="http://jabber.org/protocol/feature-neg">
      <x xmlns="jabber:x:data" type="submit">
        <field var="stream-method">
          <value>http://jabber.org/protocol/bytestreams</value>
          <value>http://jabber.org/protocol/ibb</value>
        </field>
      </x>
    </feature>
  </si>
</iq>


<iq to="receiver@smack_server/Smack" id="NK8Lh-13" type="get" from="sender@smack_server/Smack">
    <query xmlns="http://jabber.org/protocol/disco#info"/>
</iq>

<iq to="sender@smack_server/Smack" id="NK8Lh-13" type="result">
  <query xmlns="http://jabber.org/protocol/disco#info">
    <identity category="client" name="Smack" type="pc"/>
    <feature var="http://jabber.org/protocol/disco#items"/>
    <feature var="vcard-temp"/>
    <feature var="http://jabber.org/protocol/bytestreams"/>
    <feature var="http://jabber.org/protocol/ibb"/>
    <feature var="http://jabber.org/protocol/si"/>
    <feature var="http://jabber.org/protocol/xhtml-im"/>
    <feature var="jabber:x:data"/>
    <feature var="urn:xmpp:time"/>
    <feature var="jabber:iq:privacy"/>
    <feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
    <feature var="urn:xmpp:ping"/>
    <feature var="jabber:iq:last"/>
    <feature var="http://jabber.org/protocol/commands"/>
    <feature var="http://jabber.org/protocol/muc"/>
    <feature var="http://jabber.org/protocol/xdata-validate"/>
    <feature var="http://jabber.org/protocol/xdata-layout"/>
    <feature var="http://jabber.org/protocol/disco#info"/>
  </query>
</iq>

<iq to="receiver@smack_server/Smack" id="NK8Lh-25" type="set" from="sender@smack_server/Smack">
    <query xmlns="http://jabber.org/protocol/bytestreams" sid="jsi_3077759398544954943" mode="tcp">
        <streamhost jid="sender@smack_server/Smack" host="ipv6_addr1" port="7778"/>
        <streamhost jid="sender@smack_server/Smack" host="ipv4_addr1" port="7778"/>
        <streamhost jid="sender@smack_server/Smack" host="ipv6_addr2" port="7778"/>
        <streamhost jid="proxy.smack_server" host="ipv4_addr2" port="7777"/>
    </query>
</iq>

<iq to="receiver@smack_server/Smack" id="NK8Lh-26" type="set" from="sender@smack_server/Smack">
    <open xmlns="http://jabber.org/protocol/ibb" block-size="4096" sid="jsi_3077759398544954943" stanza="iq"/>
</iq>

From the best that I can tell having read the spec, it looks like everything is proceeding as it should. The sender sends the initial SI request, the receiver responds with the supported protocols (i.e., byte streams and IBB), the send then queries the receiver for all the disco items, the receiver responds with the list of features, the sender then sends various stream hosts, and then the sender sends a block of data via IBB. From there, the receiver gets the SmackException: Error in execution method, which is caused by a SmackException.NoResponseException with a message that a response was not received in 5 seconds. It looks like this question is basically being overlooked at this point, but in the hopes that someone does check it out I'd really appreciate any help. Thanks!

3
Thanks for the link. Incidentally, I had already found that documentation prior to asking this question. I think it's certainly a good idea to learn more about file transfers via XMPP. However, I would still like to see a little more documentation in the Smack library concerning what configuration settings may need to be set on the sender and receiver side.nmetts

3 Answers

3
votes

I have resolved the issue! After downloading the Smack 4.0.6 source code and debugging the packet parsing on the Receiver side, I found that everything was being done correctly. The IQs and Packets were being parsed, and the XMPPTCPConnection was correctly handling the processing of the packets. The issue turned out to be some kind of race condition. I believe the problem was here:

// transfer is of type IncomingFileTransfer, created by
// FileTransferRequet.accept()
final String fileName = transfer.getFileName();
transfer.recieveFile(new File(fileName));
LOG.info("Incoming file transfer: " + fileName);
LOG.info("Transfer status is: " + transfer.getStatus());
while (!transfer.isDone())
{
    final double progress = transfer.getProgress();
    final double progressPercent = progress * 100.0;
    String percComplete = String.format("%1$,.2f", progressPercent);
    LOG.info("Transfer status is: " + transfer.getStatus());
    LOG.info("File transfer is " + percComplete + "% complete");
    Thread.sleep(1000);
}

For some reason, the while loop was consuming all the cycles and the XMPPTCPConnection was not able to respond to the Open IQs and Data IQs in a timely manner. So I moved the progress monitoring into a new Thread, and everything worked perfectly. For reference, I was using Java version 1.8.0_31 (64-bit) on Mac OS X and also tested on Java version 1.8.0_31 (64-bit) on Windows 7 Professional (64-bit). Just to clarify, my solution worked for Smack 4.0.6. I also confirmed that it worked correctly on Java 7 (1.7.0_51) 64-bit.

0
votes

Maybe the example will provide some ideas for you:

private void sendFile(String to,String file){
    /*
     * This sends a file to someone
     * @param to the xmmp-account who receives the file, the destination  
     * @param file the path from the file
     */
    File f=new File(file);
    FileTransferManager manager = new FileTransferManager(conn);
    OutgoingFileTransfer transfer =
            manager.createOutgoingFileTransfer(to);

    // Send the file
    try {
        transfer.sendFile(f,"I have a file for you?");
    } catch (XMPPException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        sendMessage(logAccount,"Sorry,couldn't deliver the file");
    }

} 

You can find more in Examples for org.jivesoftware.smackx.filetransfer.OutgoingFileTransfer .

0
votes

There is another file receiving example which may be helpful:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 
public void startRecvFileListen(XMPPConnection conn){
    FileTransferManager manager = new FileTransferManager(conn);
    manager.addFileTransferListener(new FileTransferListener() {
        public void fileTransferRequest(FileTransferRequest request) {
            final IncomingFileTransfer inTransfer = request.accept();
            try {
                System.out.println("filename: "+request.getFileName());
                String filePath = "D:\\datas\\smackclient\\"+request.getFileName();
                inTransfer.recieveFile(new File(filePath));
                new Thread(){
                    @Override
                    public void run(){
                        long startTime = System.currentTimeMillis();
                        while(!inTransfer.isDone()){
                            if (inTransfer.getStatus().equals(Status.error)){
                                System.out.println(sdf.format(new Date())+"error!!!"+inTransfer.getError());
                            }else{
                                double progress = inTransfer.getProgress();
                                progress*=100;
                                System.out.println(sdf.format(new Date())+"status="+inTransfer.getStatus());
                                System.out.println(sdf.format(new Date())+"progress="+nf.format(progress)+"%");
                            }
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println("used "+((System.currentTimeMillis()-startTime)/1000)+" seconds  ");
                    }
                }.start();
            } catch (XMPPException e) {
                JOptionPane.showMessageDialog(null, "failed", "error", JOptionPane.ERROR_MESSAGE);
                e.printStackTrace();
            }
        }
    });
    System.out.println(connection.getUser()+"--"+connection.getServiceName());
}