3

First, sorry for english. I wanna know if you can help me to resolve this problem. What I'm trying is to download a zip that I have create from multiple byte[] using Struts2 and stream result.

I use ZipOutputStream and I have managed to create a File from it and read and download it using FileInputStream, but my problem is that I don't wan't to create a File. I just wanna convert the ZipOutputStream into InputStream (for example into ZipIntputStream) and download that ZipInputStream. For doing this I use this code:

public void downloadZip() {
    contentType = "application/octet-stream";
    filename="myZip.zip";
    ByteArrayOutputStream baos = new ByteArrayOutputStream();       
    byte[] bytes;
    try {
        ZipOutputStream zos = new ZipOutputStream(baos);
        ZipEntry ze;
        bytes = otherClass.getBytes("File1");
        ze = new ZipEntry("File1.pdf");
        zos.putNextEntry(ze);
        zos.write(bytes);
        zos.closeEntry();

        bytes = otherClass.getBytes("File2");
        ze = new ZipEntry("File2.pdf");
        zos.putNextEntry(ze);
        zos.write(bytes);
        zos.closeEntry();

        zos.flush();
        inputStream = new ZipInputStream(new ByteArrayInputStream(baos.toByteArray()));
        zos.close();
}
catch(Exception e){...}
}

My action struts.xml

<action...>
    <result name="success" type="stream">
        <param name="contentType">${contentType}</param>
        <param name="inputName">inputStream</param>
        <param name="contentDisposition">attachment;filename="${filename}"</param>
        <param name="bufferSize">1024</param>
    </result>
</action>

The problem is that the browser shows me a message says to me that the file it's not a valid zip, and its size is 0 bytes.

I hope I explained clearly, thanks a lot in advance.

Edit: As I have commented, finally I get the solution and it's very similar to leonbloy's reply. Besides return the ByteArrayInputStream I should close the ZipOutputStream before create the ByteArrayInputStream. Here's the result code, maybe it can be useful for other people:

    ...
    zos.closeEntry();                       
    zos.close();            
    inputStream = new ByteArrayInputStream(baos.toByteArray());
}
catch(Exception e){...}
}

Thanks for your help.

Airenin
  • 47
  • 1
  • 8

3 Answers3

3

The parameter <param name="inputName">inputStream</param> tells Struts2 from where to get the raw bytes that will be sent to the client. In your case, you want to send the zipped bytes. Instead, you are setting inputStream=ZipInputStream , which is a stream that takes a zipped source - to unzip sit. You don't want that, you want to send the raw zipped bytes.

REplace then

inputStream = new ZipInputStream(new ByteArrayInputStream(baos.toByteArray()))

by

inputStream = new ByteArrayInputStream(baos.toByteArray())

and it should work

leonbloy
  • 73,180
  • 20
  • 142
  • 190
  • Thanks for your answer, but it doesn't work at least for me. Maybe should I change contentType = "application/octet-stream"; for other contentType? – Airenin Nov 25 '13 at 15:19
  • Hi again. After doing more tests I get the solution. In fact I tried with your solution before read your reply, but after read it I tried it again and as I said it didn't works for me. But now I know why, so I post the solution because it can be useful for other people. The problem is that I can't close the ZipOutputStream before create the byteArrayInputStream. So thank you very much for your reply cause it made me try a few times again. – Airenin Nov 27 '13 at 15:37
1

You are lucky, this is exactly what you need.

In that specific case, I've bypassed the framework result system by writing directly to the response. That way, the Zip will be created immediately in the client system, and it will be feeded progressively, instead of waiting for the end of the elaboration and outputting it all at once with Stream result.

Then no result defined:

<action name="createZip" class="foo.bar.CreateZipAction" />

And in the Action:

public String execute() {
    try {        
        /* Read the amount of data to be streamed from Database to File System,
           summing the size of all Oracle's BLOB, PostgreSQL's ABYTE etc: 
           SELECT sum(length(my_blob_field)) FROM my_table WHERE my_conditions
        */          
        Long overallSize = getMyService().precalculateZipSize();

        // Tell the browser is a ZIP
        response.setContentType("application/zip"); 
        // Tell the browser the filename, and that it needs to be downloaded instead of opened
        response.addHeader("Content-Disposition", "attachment; filename=\"myArchive.zip\"");        
        // Tell the browser the overall size, so it can show a realistic progressbar
        response.setHeader("Content-Length", String.valueOf(overallSize));      

        ServletOutputStream sos = response.getOutputStream();       
        ZipOutputStream zos = new ZipOutputStream(sos);

        // Set-up a list of filenames to prevent duplicate entries
        HashSet<String> entries = new HashSet<String>();

        /* Read all the ID from the interested records in the database, 
           to query them later for the streams: 
           SELECT my_id FROM my_table WHERE my_conditions */           
        List<Long> allId = getMyService().loadAllId();

        for (Long currentId : allId){
            /* Load the record relative to the current ID:         
               SELECT my_filename, my_blob_field FROM my_table WHERE my_id = :currentId            
               Use resultset.getBinaryStream("my_blob_field") while mapping the BLOB column */
            FileStreamDto fileStream = getMyService().loadFileStream(currentId);

            // Create a zipEntry with a non-duplicate filename, and add it to the ZipOutputStream
            ZipEntry zipEntry = new ZipEntry(getUniqueFileName(entries,fileStream.getFilename()));
            zos.putNextEntry(zipEntry);

            // Use Apache Commons to transfer the InputStream from the DB to the OutputStream
            // on the File System; at this moment, your file is ALREADY being downloaded and growing
            IOUtils.copy(fileStream.getInputStream(), zos);

            zos.flush();
            zos.closeEntry();

            fileStream.getInputStream().close();                    
        }

        zos.close();
        sos.close();    

    } catch (Exception e) {
        logError(e);
    finally {
        IOUtils.closeQuietly(sos);
    }

    return NONE;
}
Community
  • 1
  • 1
Andrea Ligios
  • 49,480
  • 26
  • 114
  • 243
  • Thanks for your trouble, but like it happens in the Sarael answer I can't use response.getOutputStream() cause it throws an exception due to an existing call to response.getWriter() in my code. As far as I know I can only use an InputStream to send it to client using Struts2. – Airenin Nov 25 '13 at 15:44
  • Then don't call `response.getWriter()`, if you only need to donwload the ZIP (and not other data with it). The example above is part of the code I used and it totally works on Struts2. – Andrea Ligios Nov 25 '13 at 16:03
  • Also `response.getWriter()` is a PrintWriter, and it handles characters only, not binary data (like a ZIP content): http://docs.oracle.com/javaee/6/api/javax/servlet/ServletResponse.html#getWriter%28%29. This is the reason you should use OutputStream instead of PrintWriter – Andrea Ligios Nov 25 '13 at 16:06
0

ZipInputStream is for decompressing the files and accessing the uncompressed zip contents. You want to just send the compressed bytes to the browser.

After you created the zip file just send the bytes of the file using response.getOutputStream(baos.toByteArray()).

Doing everything in memory you risk running out of memory if you are compressing large files. It would be good to limit the size of the input files and give the user an error if it's too big.

Also, it looks like inputStream is a class variable. This is a very bad idea with servlets. Basically your class is not thread-safe and when two users access the page at the same time the one user might get the zip file of the other user. Instead, pass the response object as a parameter to the method or return the baos to the caller so it can write the bytes to the correct response stream. Get rid of class variables that are storing information specific to individual users and pass them between calls.

Sarel Botha
  • 12,419
  • 7
  • 54
  • 59
  • 1
    Thanks for answer so fast, but the problem is that I can't call to response.getOutputStream cause apparently I use getWriter in a .jsp. I had a problem with it few days ago. Do you know other way to resolve it? – Airenin Nov 25 '13 at 15:21
  • Don't call getWriter then, you can't call both getWriter and getOutputStream. Call getWriter if you want to send text. Call getOutputStream if you want to send the zip file. – Sarel Botha Nov 25 '13 at 15:53
  • The problem is that this call is executed in other part of the program and I can't change this, that's why I want to send an InputStream. I tried to remove that call and it seemed that it was call for an inner class or something similar cause I didn't call it, so I gave up with it and I began to use the inputStream to show contents.The problem is that if I send a pdf file to display it, it works, but it doesn't with zip file and I don't know why. – Airenin Nov 25 '13 at 16:18