Bagaimana cara menyediakan unduhan file dari kacang dukungan JSF?

91

Apakah ada cara untuk menyediakan unduhan file dari metode aksi kacang dukungan JSF? Saya telah mencoba banyak hal. Masalah utama adalah bahwa saya tidak tahu bagaimana mendapatkan OutputStreamrespon untuk menulis konten file ke. Saya tahu bagaimana melakukannya dengan Servlet, tetapi ini tidak dapat dipanggil dari formulir JSF dan membutuhkan permintaan baru.

Bagaimana saya bisa mendapatkan OutputStreamrespon dari arus FacesContext?

zmeda
sumber

Jawaban:

238

pengantar

Anda bisa menyelesaikan semuanya ExternalContext. Di JSF 1.x, Anda bisa mendapatkan HttpServletResponseobjek mentah dengan ExternalContext#getResponse(). Di JSF 2.x, Anda dapat menggunakan sekumpulan metode delegasi baru seperti ExternalContext#getResponseOutputStream()tanpa perlu mengambil HttpServletResponsedari bawah tenda JSF.

Pada tanggapannya, Anda harus mengatur Content-Typetajuk sehingga klien mengetahui aplikasi mana yang akan dikaitkan dengan file yang disediakan. Dan, Anda harus mengatur Content-Lengthtajuk sehingga klien dapat menghitung kemajuan unduhan, jika tidak maka tidak akan diketahui. Dan, Anda harus menyetel Content-Dispositiontajuk ke attachmentjika Anda menginginkan dialog Simpan Sebagai , jika tidak, klien akan mencoba menampilkannya sebaris. Terakhir, cukup tulis konten file ke aliran keluaran respons.

Bagian terpenting adalah memanggil FacesContext#responseComplete()untuk memberi tahu JSF bahwa JSF tidak boleh melakukan navigasi dan rendering setelah Anda menulis file ke respons, jika tidak, akhir respons akan tercemar dengan konten HTML halaman, atau dalam versi JSF yang lebih lama , Anda akan mendapatkan IllegalStateExceptionpesan seperti getoutputstream() has already been called for this responsesaat implementasi JSF memanggil getWriter()untuk merender HTML.

Matikan ajax / jangan gunakan perintah jarak jauh!

Anda hanya perlu memastikan bahwa metode aksi tidak dipanggil oleh permintaan ajax, tetapi dipanggil oleh permintaan normal saat Anda mengaktifkan <h:commandLink>dan <h:commandButton>. Permintaan Ajax dan perintah jarak jauh ditangani oleh JavaScript yang pada gilirannya, karena alasan keamanan, tidak memiliki fasilitas untuk memaksa dialog Simpan Sebagai dengan konten respons ajax.

Jika Anda menggunakan misalnya PrimeFaces <p:commandXxx>, maka Anda perlu memastikan bahwa Anda secara eksplisit menonaktifkan ajax via ajax="false"atribut. Jika Anda menggunakan ICEfaces, maka Anda perlu menumpuk <f:ajax disabled="true" />di komponen perintah.

Contoh JSF 2.x generik

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

Contoh JSF 1.x generik

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

Contoh file statis umum

Jika Anda perlu mengalirkan file statis dari sistem file disk lokal, gantikan kode seperti di bawah ini:

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

Contoh file dinamis umum

Jika Anda perlu melakukan streaming file yang dibuat secara dinamis, seperti PDF atau XLS, cukup sediakan di outputsana di mana API yang digunakan mengharapkan OutputStream.

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

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

Perhatikan bahwa Anda tidak dapat mengatur panjang konten di sini. Jadi, Anda perlu menghapus garis untuk menyetel panjang konten respons. Secara teknis ini tidak masalah, satu-satunya kelemahan adalah bahwa pengguna akhir akan disajikan kemajuan unduhan yang tidak diketahui. Jika ini penting, maka Anda benar-benar perlu menulis ke file lokal (sementara) terlebih dahulu dan kemudian menyediakannya seperti yang ditunjukkan pada bab sebelumnya.

Metode utilitas

Jika Anda menggunakan pustaka utilitas JSF OmniFaces , Anda dapat menggunakan salah satu dari tiga Faces#sendFile()metode praktis yang menggunakan a File, atau an InputStream, atau a byte[], dan menentukan apakah file harus diunduh sebagai attachment ( true) atau inline ( false).

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

Ya, kode ini lengkap apa adanya. Anda tidak perlu memohon responseComplete()dan seterusnya sendiri. Metode ini juga menangani dengan tepat header khusus IE dan nama file UTF-8. Anda dapat menemukan kode sumber di sini .

BalusC
sumber
1
Begitu mudah! Saya bertanya-tanya bagaimana cara membuat unduhan tersedia untuk PrimeFaces sesuai dengan showcase mereka, karena ini membutuhkan InputStreaminfrastruktur untuk p:fileDownload, dan saya belum berhasil mengonversi OutputStreamke InputStream. Sekarang jelas bahwa bahkan pendengar tindakan dapat mengubah jenis konten tanggapan dan kemudian tanggapan tersebut akan dianggap sebagai unduhan file di sisi agen pengguna. Terima kasih!
Lyubomyr Shaydariv
1
Apakah ada cara untuk melakukan ini menggunakan HTTP GET daripada HTTP POST (h: commandButton dan h: commandLink)?
Alfredo Osorio
@Alfredo: ya, menggunakan preRenderViewpemroses dalam tampilan tanpa markup. Pertanyaan serupa untuk mengunduh (baik, melayani) JSON dijawab di sini: stackoverflow.com/questions/8358006/…
BalusC
w3schools.com/media/media_mimeref.asp link rusak. Mungkin yang ini cocok: iana.org/assignments/media-types
Zakhar
2
@BalusC Anda membahas setiap topik JSF - terima kasih telah membuat hidup saya lebih mudah Pak!
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
sumber
3

Inilah yang berhasil untuk saya:

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
sumber
1
Setelah 2 hari kerja, ini menyelesaikan masalah saya dengan sedikit perubahan :) terima kasih banyak.
ÖMER TAŞCI
@ ÖMERTAŞCI: Apa yang berubah,
Kukeltje
-3

berikut adalah cuplikan kode lengkap 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();
   }
 }

Anda dapat mengubah logika membaca file jika Anda ingin file dibuat saat runtime.

Bharat Sharma
sumber
Ini hanya akan memberi Anda bagian dari file input, jika lebih besar dari 1024 byte!
hinneLinks