Ответ 1
Введение
Вы можете получить все через ExternalContext
. В JSF 1.x вы можете получить исходный HttpServletResponse
объект ExternalContext#getResponse()
. В JSF 2.x вы можете использовать кучу новых методов делегатов, таких как ExternalContext#getResponseOutputStream()
без необходимости захватывать HttpServletResponse
из-под JSF вытяжки.
В ответ вам следует установить заголовок Content-Type
, чтобы клиент знал, какое приложение будет ассоциироваться с предоставленным файлом. И вы должны установить заголовок Content-Length
, чтобы клиент мог вычислить ход загрузки, иначе он будет неизвестен. И вы должны установить заголовок Content-Disposition
на attachment
, если вы хотите открыть диалоговое окно "Сохранить как", иначе клиент попытается отобразить его в строке. Наконец, просто напишите содержимое файла в выходной поток ответа.
Наиболее важной частью является вызов FacesContext#responseComplete()
, чтобы сообщить JSF, что он не должен выполнять навигацию и рендеринг после того, как вы написали файл ответ, иначе конец ответа будет загрязнен содержимым HTML на странице или более старыми версиями JSF, вы получите IllegalStateException
с сообщением типа getoutputstream() has already been called for this response
, когда реализация JSF вызывает getWriter()
для визуализации HTML.
Общий пример JSF 2.x
public void download() throws IOException {
FacesContext fc = FacesContext.getCurrentInstance();
ExternalContext ec = fc.getExternalContext();
ec.responseReset(); // Some JSF component library or some Filter might have set some headers in the buffer beforehand. We want to get rid of them, else it may collide.
ec.setResponseContentType(contentType); // Check http://www.iana.org/assignments/media-types for all types. Use if necessary ExternalContext#getMimeType() for auto-detection based on filename.
ec.setResponseContentLength(contentLength); // Set it with the file size. This header is optional. It will work if it omitted, but the download progress will be unknown.
ec.setResponseHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); // The Save As popup magic is done here. You can give it any file name you want, this only won't work in MSIE, it will use current request URL as file name instead.
OutputStream output = ec.getResponseOutputStream();
// Now you can write the InputStream of the file to the above OutputStream the usual way.
// ...
fc.responseComplete(); // Important! Otherwise JSF will attempt to render the response which obviously will fail since it already written with a file and closed.
}
Общий пример JSF 1.x
public void download() throws IOException {
FacesContext fc = FacesContext.getCurrentInstance();
HttpServletResponse response = (HttpServletResponse) fc.getExternalContext().getResponse();
response.reset(); // Some JSF component library or some Filter might have set some headers in the buffer beforehand. We want to get rid of them, else it may collide.
response.setContentType(contentType); // Check http://www.iana.org/assignments/media-types for all types. Use if necessary ServletContext#getMimeType() for auto-detection based on filename.
response.setContentLength(contentLength); // Set it with the file size. This header is optional. It will work if it omitted, but the download progress will be unknown.
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); // The Save As popup magic is done here. You can give it any file name you want, this only won't work in MSIE, it will use current request URL as file name instead.
OutputStream output = response.getOutputStream();
// Now you can write the InputStream of the file to the above OutputStream the usual way.
// ...
fc.responseComplete(); // Important! Otherwise JSF will attempt to render the response which obviously will fail since it already written with a file and closed.
}
Пример обычного статического файла
Если вам необходимо передать статический файл из локальной файловой системы на диске, замените код следующим образом:
File file = new File("/path/to/file.ext");
String fileName = file.getName();
String contentType = ec.getMimeType(fileName); // JSF 1.x: ((ServletContext) ec.getContext()).getMimeType(fileName);
int contentLength = (int) file.length();
// ...
Files.copy(file.toPath(), output);
Пример общего динамического файла
Если вам нужно передать динамически сгенерированный файл, например PDF или XLS, просто укажите output
там, где используемый API ожидает OutputStream
.
например. iText PDF:
String fileName = "dynamic.pdf";
String contentType = "application/pdf";
// ...
Document document = new Document();
PdfWriter writer = PdfWriter.getInstance(document, output);
document.open();
// Build PDF content here.
document.close();
например. Apache POI HSSF:
String fileName = "dynamic.xls";
String contentType = "application/vnd.ms-excel";
// ...
HSSFWorkbook workbook = new HSSFWorkbook();
// Build XLS content here.
workbook.write(output);
workbook.close();
Обратите внимание, что вы не можете установить длину содержимого здесь. Поэтому вам нужно удалить строку, чтобы задать длину содержимого ответа. Это технически не проблема, единственным недостатком является то, что у конечного пользователя будет неизвестный процесс загрузки. В случае, если это важно, вам действительно нужно сначала записать в локальный (временный) файл, а затем предоставить его, как показано в предыдущей главе.
Отключите ajax!
Вам нужно только убедиться, что метод действия не вызывается с помощью запроса ajax, но он вызывается обычным запросом при срабатывании <h:commandLink>
и <h:commandButton>
. Запросы Ajax обрабатываются JavaScript, который, в свою очередь, по соображениям безопасности не имеет возможности принудительно активировать диалог "Сохранить как" с содержимым ответа ajax.
Если вы используете, например, PrimeFaces <p:commandXxx>
, тогда вам нужно убедиться, что вы явно отключите ajax с помощью атрибута ajax="false"
. Если вы используете ICEfaces, вам нужно вставить в компонент команды <f:ajax disabled="true" />
.
Полезный метод
Если вы используете служебную библиотеку JSF OmniFaces, вы можете использовать один из трех удобных Faces#sendFile()
, используя либо File
, либо InputStream
, либо byte[]
, и указывая, должен ли файл загружаться как вложение (true
) или inline (false
).
public void download() throws IOException {
Faces.sendFile(file, true);
}
Да, этот код завершен как есть. Вам не нужно вызывать responseComplete()
и так далее. Этот метод также правильно обрабатывает заголовки IE и имена файлов UTF-8. Здесь вы можете найти исходный код.