¿Cómo proporcionar una descarga de archivo desde un frijol de respaldo JSF?
-
29-10-2019 - |
Pregunta
¿Hay alguna forma de proporcionar una descarga de archivo desde un método de acción de bean de respaldo JSF? He probado muchas cosas. El principal problema es que no puedo imaginar cómo obtener el OutputStream
de la respuesta para escribir el contenido del archivo. Sé cómo hacerlo con un Servlet
, pero esto no se puede invocar desde un formulario JSF y requiere una nueva solicitud.
¿Cómo puedo conseguir el OutputStream
de la respuesta de la corriente FacesContext
?
Solución
Introducción
Puedes superar todo ExternalContext
. En JSF 1.x, puedes obtener el RAW HttpServletResponse
objeto ExternalContext#getResponse()
. En JSF 2.x, puede usar el montón de nuevos métodos delegados como ExternalContext#getResponseOutputStream()
sin la necesidad de agarrar el HttpServletResponse
Desde debajo de las campanas JSF.
En la respuesta, debe establecer el Content-Type
encabezado para que el cliente sepa qué aplicación asociar con el archivo proporcionado. Y debes establecer el Content-Length
encabezado para que el cliente pueda calcular el progreso de descarga, de lo contrario será desconocido. Y debes establecer el Content-Disposition
encabezado attachment
Si quieres un Guardar como diálogo, de lo contrario, el cliente intentará mostrarlo en línea. Finalmente, simplemente escriba el contenido del archivo en la transmisión de salida de respuesta.
La parte más importante es llamar FacesContext#responseComplete()
Para informar a JSF que no debe realizar la navegación y la representación después de haber escrito el archivo a la respuesta, de lo contrario, el final de la respuesta estará contaminado con el contenido HTML de la página, o en versiones JSF más antiguas, obtendrá un IllegalStateException
con un mensaje como getoutputstream() has already been called for this response
Cuando la implementación de JSF llama getWriter()
para renderizar html.
¡Apague Ajax / No use el comando remoto!
Solo necesita asegurarse de que el método de acción sea no llamado por una solicitud de AJAX, pero que se llama por una solicitud normal como se dispara con <h:commandLink>
y <h:commandButton>
. JavaScript manejan las solicitudes de AJAX y los comandos remotos que a su vez tienen, debido a razones de seguridad, no hay instalaciones para forzar un Guardar como Diálogo con el contenido de la respuesta AJAX.
En caso de que esté utilizando EG PrimeFaces <p:commandXxx>
, entonces debe asegurarse de que apague explícitamente Ajax a través de ajax="false"
atributo. En caso de que esté usando ICEFACES, entonces debe anidar un <f:ajax disabled="true" />
En el componente de comando.
Ejemplo genérico de 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's 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's already written with a file and closed.
}
JSF genérico 1.x Ejemplo
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's 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's already written with a file and closed.
}
Ejemplo de archivo estático común
En caso de que necesite transmitir un archivo estático desde el sistema de archivos de disco local, sustituya el código a continuación:
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);
Ejemplo de archivo dinámico común
En caso de que necesite transmitir un archivo generado dinámicamente, como PDF o XLS, simplemente proporcione output
allí donde la API que se utiliza espera un OutputStream
.
Por ejemplo, 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();
Por ejemplo, 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();
Tenga en cuenta que no puede establecer la longitud de contenido aquí. Por lo tanto, debe eliminar la línea para establecer la longitud del contenido de respuesta. Técnicamente, esto no es un problema, la única desventaja es que el enduser se presentará un progreso de descarga desconocido. En caso de que esto sea importante, entonces realmente necesita escribir en un archivo local (temporal) primero y luego proporcionarlo como se muestra en el capítulo anterior.
Método de utilidad
Si está utilizando la biblioteca de utilidad JSF Omnifacios, entonces puedes usar uno de los tres convenientes Faces#sendFile()
métodos que toman un File
, o un InputStream
, o un byte[]
, y especificando si el archivo debe descargarse como un archivo adjunto (true
) o en línea (false
).
public void download() throws IOException {
Faces.sendFile(file, true);
}
Sí, este código está completo como está. No necesitas invocar responseComplete()
y así en ti mismo. Este método también trata correctamente con encabezados específicos de IE y nombres de archivo UTF-8. Puedes encontrar código fuente aquí.
Otros consejos
public void download() throws IOException
{
File file = new File("file.txt");
FacesContext facesContext = FacesContext.getCurrentInstance();
HttpServletResponse response =
(HttpServletResponse) facesContext.getExternalContext().getResponse();
response.reset();
response.setHeader("Content-Type", "application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=file.txt");
OutputStream responseOutputStream = response.getOutputStream();
InputStream fileInputStream = new FileInputStream(file);
byte[] bytesBuffer = new byte[2048];
int bytesRead;
while ((bytesRead = fileInputStream.read(bytesBuffer)) > 0)
{
responseOutputStream.write(bytesBuffer, 0, bytesRead);
}
responseOutputStream.flush();
fileInputStream.close();
responseOutputStream.close();
facesContext.responseComplete();
}
Esto es lo que funcionó para mí:
public void downloadFile(String filename) throws IOException {
final FacesContext fc = FacesContext.getCurrentInstance();
final ExternalContext externalContext = fc.getExternalContext();
final File file = new File(filename);
externalContext.responseReset();
externalContext.setResponseContentType(ContentType.APPLICATION_OCTET_STREAM.getMimeType());
externalContext.setResponseContentLength(Long.valueOf(file.lastModified()).intValue());
externalContext.setResponseHeader("Content-Disposition", "attachment;filename=" + file.getName());
final HttpServletResponse response = (HttpServletResponse) externalContext.getResponse();
FileInputStream input = new FileInputStream(file);
byte[] buffer = new byte[1024];
final ServletOutputStream out = response.getOutputStream();
while ((input.read(buffer)) != -1) {
out.write(buffer);
}
out.flush();
fc.responseComplete();
}
Aquí está el fragmento de código completo http://bharatonjava.wordpress.com/2013/02/01/downloading-file-in-jsf-2/
@ManagedBean(name = "formBean")
@SessionScoped
public class FormBean implements Serializable
{
private static final long serialVersionUID = 1L;
/**
* Download file.
*/
public void downloadFile() throws IOException
{
File file = new File("C:\\docs\\instructions.txt");
InputStream fis = new FileInputStream(file);
byte[] buf = new byte[1024];
int offset = 0;
int numRead = 0;
while ((offset < buf.length) && ((numRead = fis.read(buf, offset, buf.length -offset)) >= 0))
{
offset += numRead;
}
fis.close();
HttpServletResponse response =
(HttpServletResponse) FacesContext.getCurrentInstance()
.getExternalContext().getResponse();
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=instructions.txt");
response.getOutputStream().write(buf);
response.getOutputStream().flush();
response.getOutputStream().close();
FacesContext.getCurrentInstance().responseComplete();
}
}
Puede cambiar la lógica de lectura de archivos en caso de que desee que el archivo se genere en tiempo de ejecución.