/*
* Copyright (C) 2013-2017 NTT DATA Corporation
*
* 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.terasoluna.gfw.web.download;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.web.servlet.view.AbstractView;
/**
* Abstract View class used for downloading binary files. <br>
* <p>
* Writes binary data to the response object. Also, depending on the requirement, the following should be implemented in
* subclass: <br>
* a) Fetching the stream that writes to the response body <br>
* b) Setting the response header information <br>
* </p>
* <p>
* In case the implementation class of this class is to be used, bean definition of
* {@code org.springframework.web.servlet.view.BeanNameViewResolver} should be done.
* </p>
* <h3>Example of bean definition file</h3>
*
* <pre>
* <bean id="beanNameViewResolver"
* class="org.springframework.web.servlet.view.BeanNameViewResolver">
* <property name="order" value="1" />
* </bean>
* </pre>
* <p>
* Next, {@code viewName} property should be defined in the bean definition of the controller which is responsible for sending
* binary data to response
* </p>
* <h3>Example of bean definition file</h3>
*
* <pre>
* <bean id=<strong style=color:red>"sampleFileDownloadView"</strong> class="org.terasoluna.gfw.web.sample.SampleFileDownloadView">
* <!-- injections.. -->
* </bean>
* </pre>
* <p>
* Further, the view name should be returned in the corresponding RequestMapping method method in a controller.
* </p>
* <h3>Example of bean definition file</h3>
*
* <pre>
* {@literal @}Controller
* {@literal @}RequestMapping("sample")
* public class SampleController {
* // ...
* {@literal @}RequestMapping("fileDownload")
* public String download(Model model) {
* // ...
* return <strong style=color:red>"sampleFileDownloadView"</strong>;
* }
* }
* </pre>
*/
public abstract class AbstractFileDownloadView extends AbstractView implements
InitializingBean {
/**
* logger
*/
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* chunk size
*/
private int chunkSize = 256;
/**
* Renders the response.
* @param model Model object
* @param request current HTTP request
* @param response current HTTP response
* @throws IOException Input/output exception
*/
@Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws IOException {
logger.debug("FileDownload start.");
InputStream inputStream = null;
OutputStream outputStream = null;
try {
// Fetching download file data
try {
inputStream = getInputStream(model, request);
} catch (IOException e) {
// In case download fails
logger.error(
"FileDownload Failed with getInputStream(). cause message is {}.",
e.getMessage());
throw e;
}
if (inputStream == null) {
throw new IOException("FileDownload Failed. InputStream is null.");
}
// Writing to output stream of HTTP response
try {
outputStream = new BufferedOutputStream(response
.getOutputStream());
} catch (IOException e) {
// In case download fails
logger.error(
"FileDownload Failed with getOutputStream(). cause message is {}.",
e.getMessage());
throw e;
}
// Editing header part
addResponseHeader(model, request, response);
try {
writeResponseStream(inputStream, outputStream);
} catch (IOException e) {
// In case download fails
logger.error(
"FileDownload Failed with writeResponseStream(). cause message is {}.",
e.getMessage());
throw e;
}
try {
outputStream.flush();
} catch (IOException e) {
// In case download fails
logger.error(
"FileDownload Failed with flush. cause message is {}.",
e.getMessage());
throw e;
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException ioe) {
logger.warn("Cannot close InputStream.", ioe);
}
}
}
}
/**
* Fetches the stream which writes to the response body
* @param model Model object
* @param request current HTTP Request
* @return stream that will write to the request
* @throws IOException Input/output exception
*/
protected abstract InputStream getInputStream(Map<String, Object> model,
HttpServletRequest request) throws IOException;
/**
* Writes the download file to the stream of HTTP response.
* @param inputStream InputStream of file data to be downloaded
* @param outputStream OutputStream of the response
* @throws IOException Input/output exception (Exception handling should be done at the caller side)
*/
protected void writeResponseStream(InputStream inputStream,
OutputStream outputStream) throws IOException {
if (inputStream == null || outputStream == null) {
return;
}
byte[] buffer = new byte[chunkSize];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
}
/**
* Adds response header
* @param model Model object
* @param request current HTTP request
* @param response current HTTP response
*/
protected abstract void addResponseHeader(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response);
/**
* Set a chunk size.
* @param chunkSize chunk size to buffer
*/
public void setChunkSize(int chunkSize) {
this.chunkSize = chunkSize;
}
/**
* Initializes the exception filter.
* <p>
* validate the chunkSize field.
* </p>
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
@Override
public void afterPropertiesSet() {
if (chunkSize <= 0) {
throw new IllegalArgumentException("chunkSize must be over 1. specified chunkSize is \""
+ chunkSize + "\".");
}
}
}