Wie kann ich einen Dateidownload von einer JSF-Backing-Bean bereitstellen?

90

Gibt es eine Möglichkeit, einen Dateidownload von einer JSF-Backing-Bean-Aktionsmethode bereitzustellen? Ich habe viele Dinge ausprobiert. Hauptproblem ist, dass ich nicht herausfinden kann, wie ich OutputStreamdie Antwort bekomme , um den Dateiinhalt zu schreiben. Ich weiß, wie es mit a geht Servlet, aber dies kann nicht über ein JSF-Formular aufgerufen werden und erfordert eine neue Anforderung.

Wie kann ich OutputStreamdie Antwort aus dem aktuellen erhalten FacesContext?

zmeda
quelle

Antworten:

234

Einführung

Sie können alles durchbringen ExternalContext. In JSF 1.x können Sie das roh erhalten HttpServletResponseObjekt durch ExternalContext#getResponse(). In JSF 2.x können Sie eine Reihe neuer Delegatenmethoden verwenden, ExternalContext#getResponseOutputStream()ohne die HttpServletResponseunter den JSF-Hauben hervorheben zu müssen.

In der Antwort sollten Sie den Content-TypeHeader so festlegen , dass der Client weiß, welche Anwendung mit der bereitgestellten Datei verknüpft werden soll. Außerdem sollten Sie den Content-LengthHeader so einstellen , dass der Client den Download-Fortschritt berechnen kann, da er sonst nicht bekannt ist. Außerdem sollten Sie den Content-DispositionHeader auf festlegen , attachmentwenn Sie ein Dialogfeld " Speichern unter" möchten. Andernfalls versucht der Client, ihn inline anzuzeigen. Zum Schluss schreiben Sie einfach den Dateiinhalt in den Antwortausgabestream.

Der wichtigste Teil ist ein Aufruf FacesContext#responseComplete(), um JSF darüber zu informieren, dass es keine Navigation und kein Rendern durchführen soll, nachdem Sie die Datei in die Antwort geschrieben haben. Andernfalls wird das Ende der Antwort mit dem HTML-Inhalt der Seite oder in älteren JSF-Versionen verschmutzt erhalten Sie IllegalStateExceptioneine Meldung, beispielsweise getoutputstream() has already been called for this responsewenn die JSF-Implementierung getWriter()zum Rendern von HTML aufruft .

Ajax ausschalten / Remote-Befehl nicht verwenden!

Sie müssen nur sicherstellen, dass die Aktionsmethode nicht von einer Ajax-Anforderung aufgerufen wird, sondern von einer normalen Anforderung, wenn Sie mit <h:commandLink>und feuern <h:commandButton>. Ajax-Anforderungen und Remote-Befehle werden von JavaScript verarbeitet, das aus Sicherheitsgründen keine Möglichkeit bietet, einen Dialog zum Speichern unter mit dem Inhalt der Ajax-Antwort zu erzwingen .

Wenn Sie zB PrimeFaces verwenden <p:commandXxx>, müssen Sie sicherstellen, dass Sie Ajax über ajax="false"Attribut explizit deaktivieren . Wenn Sie ICEfaces verwenden, müssen Sie ein <f:ajax disabled="true" />in der Befehlskomponente verschachteln .

Generisches JSF 2.x-Beispiel

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

Generisches JSF 1.x-Beispiel

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

Beispiel für eine allgemeine statische Datei

Wenn Sie eine statische Datei vom lokalen Datenträger-Dateisystem streamen müssen, ersetzen Sie den folgenden Code:

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

Beispiel für eine allgemeine dynamische Datei

Wenn Sie eine dynamisch generierte Datei wie PDF oder XLS streamen müssen, geben Sie einfach outputdort an, wo die verwendete API eine erwartet OutputStream.

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

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

Beachten Sie, dass Sie die Inhaltslänge hier nicht festlegen können. Sie müssen also die Zeile entfernen, um die Länge des Antwortinhalts festzulegen. Dies ist technisch kein Problem, der einzige Nachteil ist, dass dem Endbenutzer ein unbekannter Download-Fortschritt angezeigt wird. Wenn dies wichtig ist, müssen Sie zuerst in eine lokale (temporäre) Datei schreiben und diese dann wie im vorherigen Kapitel gezeigt bereitstellen.

Dienstprogrammmethode

Wenn Sie die JSF-Dienstprogrammbibliothek OmniFaces verwenden , können Sie eine der drei praktischen Faces#sendFile()Methoden verwenden, indem Sie entweder a File, a oder InputStreamoder a verwenden byte[]und angeben, ob die Datei als Anhang ( true) oder inline ( false) heruntergeladen werden soll .

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

Ja, dieser Code ist unverändert vollständig. Sie müssen sich nicht responseComplete()selbst aufrufen und so weiter. Diese Methode behandelt auch IE-spezifische Header und UTF-8-Dateinamen. Den Quellcode finden Sie hier .

BalusC
quelle
1
So einfach! Ich habe mich gefragt , wie der Download für PrimeFaces nach ihrer Vitrine zur Verfügung zu stellen, denn es erfordert InputStreamInfrastruktur p:fileDownload, und ich habe es nicht geschafft , wie zu konvertieren OutputStreamzu InputStream. Jetzt ist klar, dass sogar ein Aktionslistener den Antwortinhaltstyp ändern kann, und die Antwort wird dann ohnehin als Dateidownload auf der Seite des Benutzeragenten angesehen. Danke dir!
Lyubomyr Shaydariv
1
Gibt es eine Möglichkeit, dies mit einem HTTP-GET anstelle von HTTP-POST (h: commandButton und h: commandLink) zu tun?
Alfredo Osorio
@ Alfredo: Ja, mit preRenderViewListener in einer markupless Ansicht. Ähnliche Frage zum Herunterladen (gut, Serving) JSON wird hier beantwortet: stackoverflow.com/questions/8358006/…
BalusC
Der Link w3schools.com/media/media_mimeref.asp ist unterbrochen. Vielleicht ist dieser geeignet: iana.org/assignments/media-types
Zakhar
2
@BalusC Sie decken jedes jsf-Thema ab - danke, dass Sie mir das Leben leichter gemacht haben, Sir!
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
quelle
3

Das hat bei mir funktioniert:

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
quelle
1
Nach 2 Arbeitstagen löste dies mein Problem mit ein paar Änderungen :) Vielen Dank.
ÖMER TAŞCI
@ ÖMERTAŞCI: Was sich ändert,
Kukeltje
-3

Hier ist das vollständige Code-Snippet 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();
   }
 }

Sie können die Logik zum Lesen von Dateien ändern, falls die Datei zur Laufzeit generiert werden soll.

Bharat Sharma
quelle
Dadurch erhalten Sie nur dann einen Teil der Eingabedatei, wenn diese größer als 1024 Byte ist!
hinneLinks