web-dev-qa-db-de.com

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

Gibt es eine Möglichkeit, einen Dateidownload von einer JSF-Backing-Bean-Aktionsmethode bereitzustellen? Ich habe viele Dinge ausprobiert. Das Hauptproblem ist, dass ich nicht herausfinden kann, wie ich den OutputStream der Antwort erhalte, um den Dateiinhalt zu schreiben. Ich weiß, wie man es mit einem Servlet macht, aber dies kann nicht von einem JSF-Formular aus aufgerufen werden und erfordert eine neue Anfrage.

Wie kann ich das OutputStream der Antwort vom aktuellen FacesContext erhalten?

85
zmeda

Einführung

Sie können alles durch bekommen ExternalContext . In JSF 1.x können Sie das Rohobjekt HttpServletResponse mit ExternalContext#getResponse() abrufen. In JSF 2.x können Sie eine Reihe neuer Delegate-Methoden wie ExternalContext#getResponseOutputStream() verwenden, ohne das HttpServletResponse unter den JSF-Koffern hervorholen zu müssen.

In der Antwort sollten Sie den Header Content-Type So einstellen, dass der Client weiß, welche Anwendung mit der bereitgestellten Datei verknüpft werden soll. Und Sie sollten den Header Content-Length Setzen, damit der Client den Download-Fortschritt berechnen kann, da er sonst unbekannt ist. Und Sie sollten den Content-Disposition - Header auf attachment setzen, wenn Sie ein Speichern unter Dialogfeld möchten, andernfalls versucht der Client, es inline anzuzeigen. Zum Schluss schreiben Sie einfach den Dateiinhalt in den Antwortausgabestream.

Der wichtigste Teil ist der Aufruf von FacesContext#responseComplete() , um JSF mitzuteilen, dass Navigation und Rendering nicht ausgeführt werden sollen, nachdem Sie die Datei in die Antwort geschrieben haben. Andernfalls endet die Antwort Mit dem HTML-Inhalt der Seite oder in älteren JSF-Versionen verschmutzt, erhalten Sie ein IllegalStateException mit einer Meldung wie getoutputstream() has already been called for this response, wenn die JSF-Implementierung getWriter() zum Rendern aufruft HTML.

Ajax ausschalten/Remote-Befehl nicht verwenden!

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

Wenn Sie z. PrimeFaces <p:commandXxx>, Dann müssen Sie sicherstellen, dass Sie Ajax explizit über das Attribut ajax="false" Deaktivieren. Wenn Sie ICEfaces verwenden, müssen Sie ein <f:ajax disabled="true" /> In die Befehlskomponente einbetten.

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

Falls Sie eine statische Datei vom lokalen Festplattendateisystem streamen müssen, ersetzen Sie den Code wie folgt:

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 output dort an, wo die verwendete API ein OutputStream erwartet.

Z.B. 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();

Z.B. 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 hier die Inhaltslänge nicht einstellen 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 wirklich zuerst in eine lokale (temporäre) Datei schreiben und diese dann wie im vorherigen Kapitel gezeigt bereitstellen.

Utility-Methode

Wenn Sie die JSF-Hilfsprogrammbibliothek OmniFaces verwenden, können Sie eine der drei praktischen Faces#sendFile() -Methoden verwenden, die entweder File verwenden. , oder ein InputStream oder ein byte[] und die Angabe, 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 vollständig wie er ist. Sie müssen responseComplete() nicht aufrufen und so weiter. Diese Methode behandelt auch IE-spezifische Header und UTF-8-Dateinamen ordnungsgemäß. Sie finden Quellcode hier .

216
BalusC
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();

}
4
John Mendes

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();
}
3
Koray Tugay