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

Задача в следующем: у нас есть система, в которой есть страница на которой отображена некоторая отчетность. Там необходимо реализовать формирование 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