Обслуживание файлов с помощью JSF 2 / CDI с использованием URL-адресов с закладками

Мой главный вопрос: существует ли «хорошая практика» для обслуживания двоичных файлов (PDF, документов и т. д.) с использованием JSF 2 с CDI и использованием URL-адресов с закладками?

Я прочитал спецификацию JSF 2 (JSR 314 ), и я вижу, что существует параграф «Обработка ресурсов». Но, похоже, он используется только для обслуживания статических файлов, помещенных в файлы war или jar. Я действительно не понял, существует ли способ взаимодействовать здесь, зарегистрировав какой-то конкретный ResourceHandler...

На самом деле, я привык к 2 способу Seam сделать это: расширить AbstractResource с методом getResource(HttpServletRequest, HttpServletResponse) и getResourcePath() для объявления пути для обслуживания после префикса URL-адреса <webapp>/seam/resource/ и объявления SeamResourceServlet в файле web.xml.

Вот что я сделал.

Я впервые увидел Как загрузить файл, хранящийся в базе данных с помощью JSF 2.0, и попытаться реализовать его.

<f:view ...

    <f:metadata>
        <f:viewParam name="key" value="#{containerAction.key}"/>
        <f:event listener="#{containerAction.preRenderView}" type="preRenderComponent" />
    </f:metadata>

    ...

    <rich:dataGrid columns="1" value="#{containerAction.container.files}" var="file">
        <rich:panel>
                <h:panelGrid columns="2">
                    <h:outputText value="File Name:" />
                    <h:outputText value="#{file.name}" />
                </h:panelGrid>
                <h:form>
                    <h:commandButton value="Download" action="#{containerAction.download(file.key)}" />
                </h:form>
        </rich:panel>
    </rich:dataGrid>

А вот и фасоль:

@Named
@SessionScoped
public class ContainerAction {

    private Container container;

    /// Injections
    @Inject @DefaultServiceInstance
    private Instance<ContainerService> containerService;

    /// Control methods
    public void preRenderView(final ComponentSystemEvent event) {
        container = containerService.get().loadFromKey(key);
    }

    /// Action methods
    public void download(final String key) throws IOException {
        final FacesContext facesContext = FacesContext.getCurrentInstance();

        HttpServletResponse response = (HttpServletResponse) facesContext.getExternalContext().getResponse();

        final ContainerFile containerFile = containerService.get().loadFromKey(key);
        final InputStream containerFileStream = containerService.get().read(containerFile);

        response.setHeader("Content-Disposition", "attachment;filename=\""+containerFile.getName()+"\"");
        response.setContentType(containerFile.getContentType());
        response.setContentLength((int) containerFile.getSize());

        IOUtils.copy(containerFileStream, response.getOutputStream());

        response.flushBuffer();

        facesContext.responseComplete();
    }

    /// Getters / setters
    public Container getContainer() {
        return container;
    }
}

Здесь мне пришлось переключиться на Tomcat 7 (я использовал 6), чтобы правильно интерпретировать это выражение EL . С @SessionScoped работало, а с @RequestScoped нет (при нажатии на кнопку ничего не происходило).

Но потом я захотел использовать ссылку вместо кнопки.

Я попробовал <h:commandLink value="Download" action="#{containerAction.download(file.key)}" />, но он генерирует какую-то уродливую ссылку на javascript (не для закладок).

Читая спецификацию JSF 2, кажется, что есть функция «Закладка», но не совсем понятно, как ее использовать.

На самом деле это работает только с представлениями, поэтому я попытался создать пустое представление и создал h:link :

<h:link outcome="download.xhtml" value="Download">
    <f:param name="key" value="#{file.key}"/>
</h:link>
<f:view ...>
    <f:metadata>
        <f:viewParam name="key" value="#{containerFileDownloadAction.key}"/>
        <f:event listener="#{containerFileDownloadAction.download}" type="preRenderComponent" />
    </f:metadata>
</f:view>
@Named
@RequestScoped
public class ContainerFileDownloadAction {

    private String key;

    @Inject @DefaultServiceInstance
    private Instance<ContainerService> containerService;

    public void download() throws IOException {
        final FacesContext facesContext = FacesContext.getCurrentInstance();

        // same code as previously
        ...

        facesContext.responseComplete();
    }


    /// getter / setter for key
    ...
}

Но тогда у меня был java.lang.IllegalStateException: "getWriter()" has already been called for this response.

Логика, когда представление инициируется, использует getWritter для инициализации ответа.

Итак, я создал сервлет, который выполняет свою работу, и создал следующее h:outputLink :

<h:outputLink value="#{facesContext.externalContext.request.contextPath}/download/">
    <h:outputText value="Download"/>
    <f:param name="key" value="#{file.key}"/>
</h:outputLink>

Но даже если этот последний метод дает мне URL-адрес для закладки для моего файла, на самом деле это не «JFS 2»…

У вас есть совет?


person Anthony O.    schedule 08.06.2011    source источник


Ответы (3)


На самом деле существует прямое решение этой проблемы с использованием PrettyFaces URLRewriteFilter -> http://ocpsoft.org/prettyfaces/serving-dynamic-file-content-with-prettyfaces/

В этом блоге объясняется, как делать именно то, что вы хотите, без использования совершенно новой среды MVC.

person Lincoln    schedule 30.03.2012

Я согласен с BalusC. Обычно приложение представляет собой не просто приложение JSF, а приложение Java EE.

Недаром для обработки http-запросов в Java EE существуют другие вещи, помимо представлений JSF. В Java EE 6 ваш именованный компонент CDI также может быть напрямую сопоставлен с путем с помощью JAX-RS. Это альтернатива использованию сервлетов. В этом случае вы должны использовать @Produces и @Path (см., например, Входной и выходной двоичный код потоки с использованием ДЖЕРСИ?).

С другой стороны, одно из преимуществ <f:viewParam> в JSF заключается в том, что к нему можно легко прикрепить валидаторы. На данный момент ни сервлеты, ни ресурсы JAX-RS не поддерживают это.

<h:link> также более удобно использовать, чем постоянно писать <h:outputLink value="#{facesContext.externalContext.request.contextPath}/...">. Однако это можно смягчить, поместив эту часть в тег Facelets или составной компонент.

(Я думаю, было бы здорово, если бы будущая версия спецификации содержала тег ссылки в JSF для прямой ссылки на ресурсы JAX-RS (с дополнительной проверкой запуска контейнера, чтобы убедиться, что ссылка является законной)).

person Arjan Tijms    schedule 12.06.2011
comment
Да, это может сработать (хотя я не могу использовать h:link, так как это не представление, как вы сказали). Но есть ли способ с JAX-RS определить тип контента, созданный во время вызова, а не статически с помощью @Produces ? Потому что в моем случае это может варьироваться в зависимости от целевого файла для загрузки ... - person Anthony O.; 14.06.2011

JSF с самого начала разрабатывался как инфраструктура MVC, а не как файловая служба REST.

Сервлет прекрасно подходит для этой работы. Аннотируйте его с помощью @WebServlet, чтобы получить лучшее представление о Java EE 6.

person BalusC    schedule 08.06.2011
comment
Но если вы считаете, что представление — это загружаемый двоичный файл, то мне нужен MVC... спецификация JSF 2 говорит о ResponseStream в отличие от ResponseWriter: > JSF поддерживает вывод, который генерируется либо как поток байтов, либо как поток символов [. ..] Из-за ограничений базовых API-интерфейсов сервлетов для конкретного ответа можно использовать как двоичный, так и символьный вывод — их нельзя смешивать. См. Раздел 7.5 «ViewHandler», чтобы узнать, когда вызываются setResponseWriter() и setResponseStream(). И этот раздел не очень ясен ... Должен ли я создать свой собственный ViewHandler ? - person Anthony O.; 09.06.2011
comment
Да, но JSF не подходит для REST. Возможно, PrettyFaces поможет, но я сомневаюсь. - person BalusC; 09.06.2011