package gov.va.med.esr.ui.common.action;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

/**
 * This is an abstract base class that minimizes the amount of special coding
 * that needs to be written to download a file. All that is required to use
 * this class is to extend it and implement the <code>getStreamInfo()</code>
 * method so that it returns the relevant information for the file (or other
 * stream) to be downloaded. Optionally, the <code>getBufferSize()</code>
 * method may be overridden to customize the size of the buffer used to
 * transfer the file.
 *
 * @since Struts 1.2.6
 */
public abstract class DownloadAction extends PersonAbstractAction {

	/**
	 * If the <code>getBufferSize()</code> method is not overridden, this is
	 * the buffer size that will be used to transfer the data to the servlet
	 * output stream.
	 */
	protected static final int DEFAULT_BUFFER_SIZE = 1024 * 4;

	/**
	 * Returns the information on the file, or other stream, to be downloaded
	 * by this action. This method must be implemented by an extending class.
	 *
	 * @param mapping  The ActionMapping used to select this instance.
	 * @param form     The optional ActionForm bean for this request (if any).
	 * @param request  The HTTP request we are processing.
	 * @param response The HTTP response we are creating.
	 *
	 * @return The information for the file to be downloaded.
	 *
	 * @throws Exception if an exception occurs.
	 */
	protected abstract StreamInfo getStreamInfo(ActionMapping mapping,
			ActionForm form, HttpServletRequest request,
			HttpServletResponse response) throws Exception;

	/**
	 * Returns the size of the buffer to be used in transferring the data to
	 * the servlet output stream. This method may be overridden by an extending
	 * class in order to customize the buffer size.
	 *
	 * @return The size of the transfer buffer, in bytes.
	 */
	protected int getBufferSize() {
		return DEFAULT_BUFFER_SIZE;
	}

	/**
	 * Process the specified HTTP request, and create the corresponding HTTP
	 * response (or forward to another web component that will create it).
	 * Return an <code>ActionForward</code> instance describing where and how
	 * control should be forwarded, or <code>null</code> if the response has
	 * already been completed.
	 *
	 * @param mapping  The ActionMapping used to select this instance.
	 * @param form     The optional ActionForm bean for this request (if any).
	 * @param request  The HTTP request we are processing.
	 * @param response The HTTP response we are creating.
	 *
	 * @throws Exception if an exception occurs.
	 */
	public ActionForward execute(ActionMapping mapping, ActionForm form,
			HttpServletRequest request, HttpServletResponse response)
			throws Exception {

		StreamInfo info = getStreamInfo(mapping, form, request, response);
		String contentType = info.getContentType();
		InputStream stream = info.getInputStream();

		try {
			response.setContentType(contentType);
			copy(stream, response.getOutputStream());
		} finally {
			if (stream != null) {
				stream.close();
			}
		}

		// Tell Struts that we are done with the response.
		return null;
	}

	/**
	 * Copy bytes from an <code>InputStream</code> to an
	 * <code>OutputStream</code>.
	 *
	 * @param input  The <code>InputStream</code> to read from.
	 * @param output The <code>OutputStream</code> to write to.
	 *
	 * @return the number of bytes copied
	 *
	 * @throws IOException In case of an I/O problem
	 */
	public int copy(InputStream input, OutputStream output) throws IOException {
		byte[] buffer = new byte[getBufferSize()];
		int count = 0;
		int n = 0;
		while (-1 != (n = input.read(buffer))) {
			output.write(buffer, 0, n);
			count += n;
		}
		return count;
	}

	/**
	 * The information on a file, or other stream, to be downloaded by the
	 * <code>DownloadAction</code>.
	 */
	public static interface StreamInfo {

		/**
		 * Returns the content type of the stream to be downloaded.
		 *
		 * @return The content type of the stream.
		 */
		public abstract String getContentType();

		/**
		 * Returns an input stream on the content to be downloaded. This stream
		 * will be closed by the <code>DownloadAction</code>.
		 *
		 * @return The input stream for the content to be downloaded.
		 */
		public abstract InputStream getInputStream() throws IOException;
	}

	/**
	 * A concrete implementation of the <code>StreamInfo</code> interface which
	 * simplifies the downloading of a file from the disk.
	 */
	public static class FileStreamInfo implements StreamInfo {

		/**
		 * The content type for this stream.
		 */
		private String contentType;

		/**
		 * The file to be downloaded.
		 */
		private File file;

		/**
		 * Constructs an instance of this class, based on the supplied
		 * parameters.
		 *
		 * @param contentType The content type of the file.
		 * @param file        The file to be downloaded
		 */
		public FileStreamInfo(String contentType, File file) {
			this.contentType = contentType;
			this.file = file;
		}

		/**
		 * Returns the content type of the stream to be downloaded.
		 *
		 * @return The content type of the stream.
		 */
		public String getContentType() {
			return this.contentType;
		}

		/**
		 * Returns an input stream on the file to be downloaded. This stream
		 * will be closed by the <code>DownloadAction</code>.
		 *
		 * @return The input stream for the file to be downloaded.
		 */
		public InputStream getInputStream() throws IOException {
			FileInputStream fis = new FileInputStream(file);
			BufferedInputStream bis = new BufferedInputStream(fis);
			return bis;
		}
	}
	
	/**
	 * A concrete implementation of the <code>StreamInfo</code> interface which
	 * simplifies the downloading of a file from the disk.
	 */
	public static class ByteStreamInfo implements StreamInfo {

		/**
		 * The content type for this stream.
		 */
		private String contentType;

		/**
		 * The byte array to process.
		 */
		private ByteArrayInputStream byteInputStream;

		/**
		 * Constructs an instance of this class, based on the supplied
		 * parameters.
		 *
		 * @param contentType The content type of the file.
		 * @param file        The file to be downloaded
		 */
		public ByteStreamInfo(String contentType, ByteArrayInputStream byteInputStream) {
			this.contentType = contentType;
			this.byteInputStream = byteInputStream;
		}

		/**
		 * Returns the content type of the stream to be downloaded.
		 *
		 * @return The content type of the stream.
		 */
		public String getContentType() {
			return this.contentType;
		}

		/**
		 * Returns an input stream on the file to be downloaded. This stream
		 * will be closed by the <code>DownloadAction</code>.
		 *
		 * @return The input stream for the file to be downloaded.
		 */
		public InputStream getInputStream() throws IOException {
			BufferedInputStream bis = new BufferedInputStream(byteInputStream);
			return bis;
		}
	}
	
	/**
	 * A concrete implementation of the <code>StreamInfo</code> interface which
	 * simplifies the downloading of a web application resource.
	 */
	public static class ResourceStreamInfo implements StreamInfo {

		/**
		 * The content type for this stream.
		 */
		private String contentType;

		/**
		 * The servlet context for the resource to be downloaded.
		 */
		private ServletContext context;

		/**
		 * The path to the resource to be downloaded.
		 */
		private String path;

		/**
		 * Constructs an instance of this class, based on the supplied
		 * parameters.
		 *
		 * @param contentType The content type of the file.
		 * @param context     The servlet context for the resource.
		 * @param path        The path to the resource to be downloaded.
		 */
		public ResourceStreamInfo(String contentType, ServletContext context,
				String path) {
			this.contentType = contentType;
			this.context = context;
			this.path = path;
		}

		/**
		 * Returns the content type of the stream to be downloaded.
		 *
		 * @return The content type of the stream.
		 */
		public String getContentType() {
			return this.contentType;
		}

		/**
		 * Returns an input stream on the resource to be downloaded. This stream
		 * will be closed by the <code>DownloadAction</code>.
		 *
		 * @return The input stream for the resource to be downloaded.
		 */
		public InputStream getInputStream() throws IOException {
			return context.getResourceAsStream(path);
		}
	}
}
