RichFaces 3.0, Формирование и выгрузка файла, jQuery и Костыли

в 11:04, , рубрики: java, javascript, javascript events, jquery, jsf, костыли, Песочница, метки: , , , , ,

RichFaces 3.0, Формирование и выгрузка файла, jQuery и Костыли

Приветствую, дорогой читатель! В данной статье я хотел изложить одну проблему, с которой я столкнулся при разработке, а также способ ее решения. Решение конечно не самое безупречное, но имеет место быть. Если вам что-то не понравиться, или вы знаете решение лучше, прошу большими огурцами меня не бить, так как я еще мал и зелен. Бейте маленькими с комментариями и поучениями.

Задача в следующем: у нас есть система, в которой есть страница на которой отображена некоторая отчетность. Там необходимо реализовать формирование Excel файла и выгрузку его для пользователя.

Страница имеет следующую приблизительную структуру, которая отображена на верхнем рисунке.

Причем особенностью является то, что таблица очень большая, и выгружать в Excel нужно много записей.
А при нажатии на строку выводиться панелька для редактирования записи.
Код формирующий файл:

public void generatePatientsExcel() {
try{
     log.info("Generating patients excel"); 
   
     FacesContext facesContext = FacesContext.getCurrentInstance();
     ExternalContext externalContext = facesContext.getExternalContext();
     HttpServletResponse response = (HttpServletResponse) externalContext.getResponse();
        
     //Create excel file    
     Workbook wb = new HSSFWorkbook();
 
     //
     //
     //
     //
     //
          
      // Header
      response.setHeader("Content-disposition",  "attachment; filename=1.xls");
      response.setHeader("Cache-Control",   "must-revalidate, post-check=0, pre-check=0");
      response.setCharacterEncoding("UTF-8");
      response.setContentType("application/vnd.ms-excel");

      //Send Response
      ServletOutputStream responseOutputStream = response.getOutputStream();
      wb.write(responseOutputStream);
      responseOutputStream.flush();
      responseOutputStream.close();
      facesContext.responseComplete();

       log.info("Generating patients excel comlplete");

       } 
catch (Exception ex) 
      {
            ex.printStackTrace();
       }
} 

Ну и собственно проблема

Проблема состоит в том, что при выгрузке файла формирование его происходит некоторое время, и если в этот момент нажать на строку то мы получим:

org.jboss.seam.ConcurrentRequestTimeoutException: Concurrent call to conversation

Чтобы этого не случилось необходимо навесить на onclick перед формированием файла JavaScript функцию, которая отображает Loader. И после action скрыть его тоже JavaScript функцией навешеной на oncomplete. Вроде все просто, не правда ли?

Подводные камни

Так как мы формируем файл динамически, мы не сохраняем его на диск, что могло б облегчить задачу, путем возвращения не HttpServletResponse, в котором файл, а переходом по ссылке, где сохранен файл на диске. Но это в свою очередь будет есть дисковое пространство сервера, поэтому динамическое формирование файла это то, что нам нужно.

Загвоздка в том, что кнопка h:commandButton не имеет oncomplete. И вообще все елементы «h:» не имеют этого event. Подумав немного: «Вообще зачем нам этот h:commandButton? Ведь у нас есть волшебный a4j:commandButton».

Реализуем выгрузку файла через a4j:commandButton, это выглядит так:

<a4j:commandButton  onclick="showLoader();" image="exportExcel.png"
action="#{importToExcelBean.getExcelFile()}" oncomplete="hideLoader();"/>

И все вроде бы прекрасно, но это только на первый взгляд. Так как a4j:commandButton выгружает HttpServletResponse в текущую страницу.

Пользователь выгружает файл в Excel формате, а ему открывается страница с корявками. «Непорядок»- сказал пользователь, ударив кулаком в клавиатуру, матерясь. И разочаровался в нашей системе на всю жизнь.

Можно попытаться на h:commandButton навесить a4j:support, который на onsubmit будет отображать лоадер, при action формировать файл, а на oncomplete скрывать лоадер. Но проблема в том что a4j:support не может вернуть HttpServletResponse с сервера. Он это делает тоже в страницу. Так как и все элементы a4j возвращают HttpServletResponse в страницу.

Незадача получается. Что делать, никто не знает, интернет молчит, пользователи плачут, и сам сидишь как укакался. А ведь такой простой event, но его так не хватает.

Решение, Костыли и jQuery

Решение пришло в голову коллеге. Все довольно просто.

  1. Делим процедуру формирования файла на две: генерацию и возвращение запроса.
  2. Создаем a4j:commandButton, который отвечает за формирование файла без его возвращения, отображение лоадера и его скрытие.
    <a4j:commandButton  onclick="showLoader();" image="exportExcel.png"  
    action="#{importToExcelBean.generateExcel()}" oncomplete="hideLoader()"/>
  3. Создаем скрытый h:commandButton, который возвращает файл, а также присваиваем ему id для последующего поиска.
    <h:commandButton id="exelFileButton"  style=" visibility: hidden;"  
    action="#{importToExcelBean.returnResponse()}" />
  4. Создаем JavaScript функцию, которая с помошью jQuery находит скрытый h:commandButton по id и нажимает его.
    <script type="text/javascript" >
     function  clickButtonGetExelFile(){jQuery("#exelFileButton").click();}
    </script>

  5. Навешиваем нашу JavaScript функцию после скрытия лоадера.
    <a4j:commandButton  onclick="showLoader();" image="exportExcel.png" 
    action="#{importToExcelBean.generatePatientsExcel()}"  oncomplete="hideLoader();clickButtonGetExelFile();"/>

Готово, пользователь получает файл в том виде, в котором должен получать. Критическая ситуация разрешена.

image

Автор: jorikburlakov


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js