Jak zapewnić pobieranie pliku z komponentu bean zapasowego JSF?

91

Czy istnieje sposób na zapewnienie pobierania pliku z metody akcji wspierającej komponent bean JSF? Próbowałem wielu rzeczy. Główny problem polega na tym, że nie mogę pojąć, jak uzyskać OutputStreamodpowiedź, aby zapisać zawartość pliku. Wiem, jak to zrobić Servlet, ale nie można tego wywołać z formularza JSF i wymaga nowego żądania.

Jak mogę uzyskać OutputStreamodpowiedź z obecnego FacesContext?

zmeda
źródło

Odpowiedzi:

238

Wprowadzenie

Możesz przejść wszystko ExternalContext. W JSF 1.x możesz pobrać surowy HttpServletResponseobiekt przez ExternalContext#getResponse(). W JSF 2.x możesz użyć wielu nowych metod delegatów, ExternalContext#getResponseOutputStream()bez potrzeby chwytania ich HttpServletResponsespod masek JSF.

W odpowiedzi należy ustawić Content-Typenagłówek, aby klient wiedział, którą aplikację skojarzyć z podanym plikiem. I powinieneś ustawić Content-Lengthnagłówek tak, aby klient mógł obliczyć postęp pobierania, w przeciwnym razie będzie on nieznany. I powinieneś ustawić Content-Dispositionnagłówek na, attachmentjeśli chcesz okno dialogowe Zapisz jako , w przeciwnym razie klient spróbuje wyświetlić je w tekście. Na koniec po prostu zapisz zawartość pliku w strumieniu wyjściowym odpowiedzi.

Najważniejszą częścią jest wezwanie FacesContext#responseComplete()do poinformowania JSF, że nie powinno wykonywać nawigacji i renderowania po zapisaniu pliku do odpowiedzi, w przeciwnym razie koniec odpowiedzi zostanie zanieczyszczony treścią HTML strony lub w starszych wersjach JSF , otrzymasz IllegalStateExceptionkomunikat podobny do tego, getoutputstream() has already been called for this responsegdy implementacja JSF wywołuje getWriter()renderowanie HTML.

Wyłącz Ajax / nie używaj zdalnego polecenia!

Musisz tylko upewnić się, że metoda akcji nie jest wywoływana przez żądanie AJAX, ale jest wywoływana przez normalne żądanie podczas uruchamiania z <h:commandLink>i <h:commandButton>. Żądania Ajax i polecenia zdalne są obsługiwane przez JavaScript, który z kolei, ze względów bezpieczeństwa, nie ma możliwości wymuszenia dialogu Zapisz jako z treścią odpowiedzi Ajax .

W przypadku, gdy używasz np. PrimeFaces <p:commandXxx>, musisz upewnić się, że jawnie wyłączyłeś Ajax poprzez ajax="false"atrybut. Jeśli używasz ICEfaces, musisz zagnieździć <f:ajax disabled="true" />w komponencie poleceń.

Ogólny przykład 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.
}

Ogólny przykład 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.
}

Przykład typowego pliku statycznego

Jeśli chcesz przesłać strumieniowo plik statyczny z lokalnego systemu plików dysku, zastąp kod jak poniżej:

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

Przykład typowego pliku dynamicznego

Jeśli chcesz przesłać strumieniowo dynamicznie generowany plik, taki jak PDF lub XLS, po prostu podaj outputtam, gdzie używane API oczekuje pliku OutputStream.

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

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

Zwróć uwagę, że nie możesz tutaj ustawić długości treści. Musisz więc usunąć wiersz, aby ustawić długość treści odpowiedzi. Z technicznego punktu widzenia nie stanowi to problemu, jedyną wadą jest to, że użytkownikowi końcowemu zostanie przedstawiony nieznany postęp pobierania. W przypadku, gdy jest to ważne, naprawdę musisz najpierw zapisać do lokalnego (tymczasowego) pliku, a następnie dostarczyć go, jak pokazano w poprzednim rozdziale.

Metoda użytkowa

Jeśli korzystasz z biblioteki narzędziowej JSF OmniFaces , możesz użyć jednej z trzech wygodnych Faces#sendFile()metod, przyjmując a File, lub an InputStream, lub a byte[]i określając, czy plik powinien zostać pobrany jako załącznik ( true) czy inline ( false).

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

Tak, ten kod jest kompletny bez zmian. Nie musisz wzywać siebie responseComplete()i tak dalej. Ta metoda również poprawnie radzi sobie z nagłówkami specyficznymi dla IE i nazwami plików UTF-8. Możesz znaleźć kod źródłowy tutaj .

BalusC
źródło
1
Tak łatwo! Zastanawiałem się, jak udostępnić pobieranie dla PrimeFaces zgodnie z ich wizytówką, ponieważ wymaga to InputStreaminfrastruktury p:fileDownload, a nie udało mi się przekonwertować OutputStreamna InputStream. Teraz jest jasne, że nawet detektor akcji może zmienić typ zawartości odpowiedzi, a wtedy odpowiedź i tak zostanie uwzględniona jako plik do pobrania po stronie klienta użytkownika. Dziękuję Ci!
Lyubomyr Shaydariv
1
Czy można to zrobić za pomocą protokołu HTTP GET zamiast HTTP POST (h: commandButton i h: commandLink)?
Alfredo Osorio
@Alfredo: tak, używając preRenderViewdetektora w widoku bez znaczników. Odpowiedzi na podobne pytanie dotyczące pobierania (dobrze, serwowania) JSON jest tutaj: stackoverflow.com/questions/8358006/ ...
BalusC
Link w3schools.com/media/media_mimeref.asp jest uszkodzony. Może ten jest odpowiedni: iana.org/ assignments
Zakhar
2
@BalusC Pan porusza każdy temat jsf - dziękuję za ułatwienie mi życia!
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
źródło
3

Oto, co zadziałało dla mnie:

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
źródło
1
Po 2 dniach pracy rozwiązało to mój problem z niewielkimi zmianami :) bardzo dziękuję.
ÖMER TAŞCI
@ ÖMERTAŞCI: Co się zmienia,
Kukeltje
-3

tutaj jest pełny fragment kodu 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();
   }
 }

Możesz zmienić logikę odczytu plików, jeśli chcesz, aby plik był generowany w czasie wykonywania.

Bharat Sharma
źródło
Dostaniesz tylko część pliku wejściowego, jeśli jest większy niż 1024 bajty!
hinneLinks