servlet

Java Servlet Generate Zip File Example

In this article we illustrate how to generate a zip (compressed) file via a GET request to a custom servlet.

The servlet will serve said file (configurable via a WebInitParam) to the browser and the browser will then prompt the user with a download prompt to save said file to the file system.

The file will be served asynchronously using FileChannels transferTo(...).

1. Introduction

To generate a zip file from a servlet we will be using a combination of FileChannel, ZipOuputStream, ZipEntry and asynchronous processing from within a WebServlet.

 
The servlet will be configured with various meta-data specifying the timeout for the asynchronous processing, the file to serve, and a Runnable implementation that handles the actual asynchronous processing of reading and writing the file to the ZipOutputStream.

The program will be run from the command line using maven, specifically utilizing the maven cargo plugin to automatically and seamlessly deploy our code and run it without the need for explicitly installing and setting up a servlet 3.1 compliant container.

2. Technologies used

The example code in this article was built and run using:

  • Java 8
  • Maven 3.3.9
  • STS (3.9.0.RELEASE)
  • Ubuntu 16.04 (Windows, Mac or Linux will do fine)

3. Setup

To ensure that is installed the correct version of Maven and Java you can execute the following:

Confirming Setup

jean-jay@jeanjay-SATELLITE-L750D:~$ java -version
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)
jean-jay@jeanjay-SATELLITE-L750D:~$ javac -version
javac 1.8.0_101
jean-jay@jeanjay-SATELLITE-L750D:~$ mvn -version
Apache Maven 3.3.9
Maven home: /usr/share/maven
Java version: 1.8.0_101, vendor: Oracle Corporation
Java home: /home/jean-jay/runtimes/jdk1.8.0_101/jre
Default locale: en_ZA, platform encoding: UTF-8
OS name: "linux", version: "4.10.0-37-generic", arch: "amd64", family: "unix"
jean-jay@jeanjay-SATELLITE-L750D:~$

4. Maven Cargo Plugin

Cargo is a wrapper that allows us to do programmatic manipulation of Containers, in our case servlet containers, in a standardized way.

The maven cargo plugin allows us to easily, and as part of the maven build process, deploy and run our application from the command line.

Below follows our maven cargo plugin configuration: (Using version 1.6.4)

Maven Cargo Plugin Configuration

<plugin>
				<groupId>org.codehaus.cargo</groupId>
				<artifactId>cargo-maven2-plugin</artifactId>
				<configuration>
					<container>
						<containerId>tomcat8x</containerId>
						<artifactInstaller>
							<groupId>org.apache.tomcat</groupId>
							<artifactId>tomcat</artifactId>
							<version>${tomcat.version}</version>
						</artifactInstaller>
					</container>
					<configuration>
						<type>standalone</type>
						<home>
							${project.build.directory}/apache-tomcat-${tomcat.version}
						</home>
						<properties>
							<cargo.servlet.port>8080</cargo.servlet.port>
							<cargo.logging>high</cargo.logging>
						</properties>
					</configuration>
					<deployables>
						<deployable>
							<groupId>${project.groupId}</groupId>
							<artifactId>${project.artifactId}</artifactId>
							<type>war</type>
							<properties>
								<context>/sample</context>
							</properties>
						</deployable>
					</deployables>
				</configuration>
			</plugin>
  • lines 7-11: uses maven to find and download the relevant version of Tomcat (8.x) we want
  • line 16: configure our container to be a standalone instance and place it in a specific directory
  • lines 24-31: we specify the artifact to deploy, type of packaging and the context route

5. Channels

It wasn’t strictly necessary to use Channels, we could have just as easily handled reading from the input File and writing to the ZipOutputStream manually.

Channels, however, abstract away some of the ugliness of doing said tasks as well as provide us with some potential performance optimizations if we leverage the transferTo(...) method of FileChannel.

Below follows the snippet of code that handles the creation of the input and output FileChannels and the transferring of bytes to and from.

FileChannel usage

...
WritableByteChannel outputChannel = Channels.newChannel(zipStream);
FileChannel inputChannel = FileChannel.open(this.srcPath, StandardOpenOption.READ);
...
private void transferContents(final FileChannel inputChannel, final WritableByteChannel outputChannel) throws IOException {
            long size = inputChannel.size();
            long position = 0;
            while (position < size) {
                position += inputChannel.transferTo(position, MAX_BYTE_COUNT, outputChannel);
            }
 }
...
  • line 2: we create a WritableByteChannel by wrapping the ZipOutputStream, this will be the Channel to which we write the File contents
  • line 3: we create an input FileChannel to the input File
  • lines 8-10: in a loop we transfer the contents from one Channel to the other. The MAX_BYE_COUNT is the maximum number of bytes we will transfer at any given loop instance (determined by some windows limitation which results in a slow copy should the bytes exceed this magic number)

6. Why Asynchronous

It is quite common for most IO operations to become the bottleneck of any system, therefore we have opted to handle the processing of the File asynchronously to better illustrate part of the idiomatic preference when dealing with similar use cases.

I say part, because we could also have instead used a WriteListener, to efficiently handle dispatching the contents to the ZipOutputStream .

In the sample program we ensure our servlet can support asynchronous mode and upon request we immediately set the HttpServletRequest into asynchronous mode.

This allows the request to be handled in a dedicated, alternate thread, from another thread pool, designed specifically for the purpose of serving the zip File. Ordinarily we would like to keep thread pools designed for high CPU activity separate from thread pools designed for IO hence us defining a thread pool specifically for serving our zip File.

Servlet showing asynchronous mode settings

@WebServlet(urlPatterns = "/zipfile", initParams = { @WebInitParam(name = "src", value = "/tmp/sample.mp4"), @WebInitParam(name = "timeout", value = "5000") }, asyncSupported = true)
public final class ZipFileServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private static final String TIMEOUT_PARAM_KEY = "timeout";
    private static final String SRC_PATH = "src";

    @Override
    protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
        final AsyncContext asyncCtx = request.startAsync();
        asyncCtx.setTimeout(Long.valueOf(getServletConfig().getInitParameter(TIMEOUT_PARAM_KEY)));

        getExecutorService(request).execute(new ZipFileHandler(response, getServletConfig().getInitParameter(SRC_PATH)));
    }

    private ExecutorService getExecutorService(final HttpServletRequest request) {
        assert !Objects.isNull(request);
        return (ExecutorService) request.getServletContext().getAttribute(ContextListenerExecutorServiceInitializer.THREAD_POOL_EXECUTOR);
    }   
       ...
  • line 1: we configure certain attributes for our servlet including, importantly, a timeout for asynchronous processing and a setting indicating our servlet supports asynchronous processing
  • lines 10-11: we immediately set our HttpServletRequestinto asynchronous mode
  • lines 16-19: we retrieve an ExecutorService from the ServletContextso that we can submit our File serving code

7. Required Headers

The following headers are used to indicate to the client (browser) the content type and the content disposition. The content type indicates application/zip (self explanatory) but the content disposition begs some clarity.

Content disposition simply indicates to the browser how the content should be displayed, inline in the browser (web page content) or as an attachment (downloadable as a file).

Setting required headers

private static final String ZIP_CONTENT_TYPE = "application/zip";
private static final String CONTENT_DISPOSITION_KEY = "Content-Disposition";
private static final String CONTENT_DISPOSITION_VALUE = "attachment; filename=\"%s\"";

this.response.setContentType(ZIP_CONTENT_TYPE);
this.response.setHeader(CONTENT_DISPOSITION_KEY, String.format(CONTENT_DISPOSITION_VALUE, fileName + ".zip"));
  • In the above snippet of code we illustrate the setting of required headers to ensure the browser will respect that fact that we want the user to be challenged as to where to save the incoming File.

8. Running the Program

The core of the File processing is done from within a Runnable which is submitted to the ExecutorService (thread pool) to be processed in a different thread.

Runnable for processing the File

 private static final class ZipFileHandler implements Runnable {

        private static final long MAX_BYTE_COUNT = 67076096l;
        private static final String ZIP_CONTENT_TYPE = "application/zip";
        private static final String CONTENT_DISPOSITION_KEY = "Content-Disposition";
        private static final String CONTENT_DISPOSITION_VALUE = "attachment; filename=\"%s\"";

        private final HttpServletResponse response;
        private final Path srcPath;

        private ZipFileHandler(final HttpServletResponse response, final String srcPath) {
            assert !Objects.isNull(response) && !Objects.isNull(srcPath);

            this.response = response;
            this.srcPath = Paths.get(srcPath);
        }

        @Override
        public void run() {
            try (ZipOutputStream zipStream = new ZipOutputStream(this.response.getOutputStream());
                    WritableByteChannel outputChannel = Channels.newChannel(zipStream);
                    FileChannel inputChannel = FileChannel.open(this.srcPath, StandardOpenOption.READ);) {

                final String fileName = this.srcPath.getFileName().toString();

                initResponse(response, fileName);

                zipStream.putNextEntry(new ZipEntry(fileName));

                transferContents(inputChannel, outputChannel);

                zipStream.flush();
                this.response.getOutputStream().flush();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    this.response.getOutputStream().close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        private void initResponse(final HttpServletResponse response, final String fileName) {
            assert !Objects.isNull(response) && !Objects.isNull(fileName) && !fileName.isEmpty();

            this.response.setContentType(ZIP_CONTENT_TYPE);
            this.response.setHeader(CONTENT_DISPOSITION_KEY, String.format(CONTENT_DISPOSITION_VALUE, fileName + ".zip"));
        }

        private void transferContents(final FileChannel inputChannel, final WritableByteChannel outputChannel) throws IOException {
            long size = inputChannel.size();
            long position = 0;
            while (position < size) {
                position += inputChannel.transferTo(position, MAX_BYTE_COUNT, outputChannel);
            }
        }
    }
  • line 20: we create a ZipOutputStream which wraps the HttpServletResponse OutputStream
  • line 21: we create a WritableByteChannel which wraps the ZipOutputStream, this way we can work with Channels and also ensure anything written to the WritableByteChannel will get compressed if possible
  • line 22: we create a FileChannel to ensure we can leverage the transferTo(..)optimizations between Channels whilst transferring the File from input to output
  • line 28: we add a ZipEntry for the File, as expected a zipped file can contain more than 1 File

Before building and running the program one can specify where to find a file to serve. This can be configured in the class ZipFileServlet via the WebInitParam src. Simply specify a file path to serve and the file will be served zipped via the ZipFileServlet.

After downloading the sample project and configuring a file to server, navigate to the project root folder and execute the following:

  • build: mvn clean install package
  • run: mvn cargo:run

Navigate to localhost:8080/sample/zipfile and you will be prompted to save the zip file.

9. Summary

In this example we illustrated how to generate a zip file from a servlet in an asynchronous manner.

We also illustrated how to setup and run our application using maven and more specifically the maven cargo plugin which allowed us to run our application without the need for explicitly installing and setting up a servlet 3.1 compliant container.

10. Download the Source Code

This was a Java Servlet Generate Zip File Example.

Download
You can download the full source code of this example here: Java Servlet Generate Zip File Example

JJ

Jean-Jay Vester graduated from the Cape Peninsula University of Technology, Cape Town, in 2001 and has spent most of his career developing Java backend systems for small to large sized companies both sides of the equator. He has an abundance of experience and knowledge in many varied Java frameworks and has also acquired some systems knowledge along the way. Recently he has started developing his JavaScript skill set specifically targeting Angularjs and also bridged that skill to the backend with Nodejs.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button