Como fornecer um download de arquivo de um backing bean JSF?

90

Existe alguma maneira de fornecer um download de arquivo de um método de ação de bean de apoio JSF? Eu tentei muitas coisas. O principal problema é que não consigo descobrir como obter o OutputStreamda resposta para gravar o conteúdo do arquivo. Eu sei fazer isso com um Servlet, mas isso não pode ser chamado de um formulário JSF e requer uma nova solicitação.

Como posso obter o resultado OutputStreamda resposta do atual FacesContext?

zmeda
fonte

Respostas:

236

Introdução

Você pode fazer tudo passar ExternalContext. No JSF 1.x, você pode obter o HttpServletResponseobjeto bruto ExternalContext#getResponse(). No JSF 2.x, você pode usar um monte de novos métodos delegados, como ExternalContext#getResponseOutputStream()sem a necessidade de pegar HttpServletResponseo JSF sob os capuzes.

Na resposta, você deve definir o Content-Typecabeçalho para que o cliente saiba qual aplicativo associar ao arquivo fornecido. E, você deve definir o Content-Lengthcabeçalho para que o cliente possa calcular o andamento do download, caso contrário, será desconhecido. E, você deve definir o Content-Dispositioncabeçalho para attachmentse quiser uma caixa de diálogo Salvar como , caso contrário, o cliente tentará exibi-la embutida. Finalmente, basta escrever o conteúdo do arquivo no fluxo de saída da resposta.

A parte mais importante é chamar FacesContext#responseComplete()para informar ao JSF que ele não deve realizar navegação e renderização após você ter escrito o arquivo para a resposta, caso contrário, o final da resposta será poluído com o conteúdo HTML da página, ou em versões mais antigas do JSF , você receberá um IllegalStateExceptioncom uma mensagem como getoutputstream() has already been called for this responsequando a implementação JSF chama getWriter()para renderizar HTML.

Desligue o ajax / não use o comando remoto!

Você só precisa se certificar de que o método de ação não seja chamado por uma solicitação ajax, mas que seja chamado por uma solicitação normal conforme você dispara com <h:commandLink>e <h:commandButton>. Solicitações Ajax e comandos remotos são manipulados por JavaScript que, por sua vez, por motivos de segurança, não tem recursos para forçar um diálogo Salvar como com o conteúdo da resposta ajax.

No caso de você estar usando, por exemplo <p:commandXxx>, PrimeFaces , então você precisa certificar-se de desligar explicitamente o ajax via ajax="false"atributo. Caso você esteja usando ICEfaces, então você precisa aninhar um <f:ajax disabled="true" />no componente de comando.

Exemplo 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.
}

Exemplo genérico de 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'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.
}

Exemplo comum de arquivo estático

Caso você precise transmitir um arquivo estático do sistema de arquivos do disco local, substitua o código conforme abaixo:

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);

Exemplo de arquivo dinâmico comum

Caso precise transmitir um arquivo gerado dinamicamente, como PDF ou XLS, basta fornecer outputonde a API em uso espera um OutputStream.

Por exemplo, 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 exemplo, 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();

Observe que você não pode definir o comprimento do conteúdo aqui. Portanto, você precisa remover a linha para definir o comprimento do conteúdo da resposta. Tecnicamente, isso não é problema, a única desvantagem é que o usuário final verá um progresso de download desconhecido. Caso isso seja importante, você realmente precisa gravar em um arquivo local (temporário) primeiro e, em seguida, fornecê-lo conforme mostrado no capítulo anterior.

Método de utilidade

Se você estiver usando a biblioteca de utilitários JSF OmniFaces , poderá usar um dos três Faces#sendFile()métodos convenientes usando a File, ou an InputStream, ou a byte[]e especificando se o arquivo deve ser baixado como um anexo ( true) ou embutido ( false).

public void download() throws IOException {
    Faces.sendFile(file, true);
}

Sim, este código está completo como está. Você não precisa invocar responseComplete()e assim por diante. Este método também lida corretamente com cabeçalhos específicos do IE e nomes de arquivo UTF-8. Você pode encontrar o código-fonte aqui .

BalusC
fonte
1
Tão fácil! Ando me perguntando como disponibilizar o download para o PrimeFaces de acordo com seu showcase, pois requer InputStreaminfraestrutura para p:fileDownload, e ainda não consegui converter OutputStreampara InputStream. Agora está claro que mesmo um ouvinte de ação pode alterar o tipo de conteúdo da resposta e, de qualquer maneira, a resposta será respeitada como um download de arquivo no lado do agente do usuário. Obrigado!
Lyubomyr Shaydariv
1
Existe uma maneira de fazer isso usando HTTP GET em vez de HTTP POST (h: commandButton e h: commandLink)?
Alfredo Osorio
@Alfredo: sim, usando o preRenderViewouvinte em uma visualização sem marcação. Pergunta semelhante para baixar (bem, servir) JSON é respondida aqui: stackoverflow.com/questions/8358006/…
BalusC
O link w3schools.com/media/media_mimeref.asp está quebrado. Talvez este seja adequado: iana.org/assignments/media-types
Zakhar
2
@BalusC você cobre todos os tópicos jsf - obrigado por tornar minha vida mais fácil, senhor!
Buttinger Xaver
5
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();

}
John mendes
fonte
3

Isto é o que funcionou para mim:

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();
}
Koray Tugay
fonte
1
Após 2 dias de trabalho, isso resolveu meu problema com pequenas mudanças :) muito obrigado.
ÖMER TAŞCI
@ ÖMERTAŞCI: O que muda,
Kukeltje
-3

aqui está o snippet 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();
   }
 }

Você pode alterar a lógica de leitura do arquivo caso queira que o arquivo seja gerado em tempo de execução.

Bharat Sharma
fonte
Isso só obterá parte do arquivo de entrada, se for maior que 1024 bytes!
hinneLinks