Получение файла с сервера с обработкой возможных ошибок

    Для одной из наших интранет-систем мы делали простой поиск по содержимым файлов, присоединённых к официальным документам. Результатом поиска был список имён файлов и ссылок на сервлет, эти файлы выгружающий. Сервлет читает файл по его идентификатору из хранилища и выдаёт его с «Content-Type: application/octet-stream» или MIME-типу, соответствующему файлу. Но как поступить, если на сервере произошла ошибка, как сказать об этом оператору? Можно было бы устроить переадресацию на страницу с сообщением, но это неудобно — надо возвращаться назад, где введённые в формы данные потеряны.
    С другой стороны, можно вызвать сервлет через AJAX XmlHttpRequest и вывести сообщение об ошибке, но как же тогда вернуть файл? Функции обратных вызовов объекта XHR не работают с пришедшими с сервера двоичными данными и не смогут показать стандартный браузерный диалог «Сохранить/загрузить файл».

    Вышли из положения таким образом. Клиент вызывает сервлет два раза. На шаге 1 он просит его загрузить файл из серверного хранилища (по технологии AJAX передавая все необходимые для этого параметры), сервлет вычитывает файл и кладёт его содержимое, имя, MIME-тип и прочие атрибуты в сессию, а клиенту отвечает (формат ответа JSON) либо неким session_id (документ успешно получен и ждёт клиента), либо строкой ошибки, которую Javascript на клиенте легко покажет через window.alert(). Получив на шаге 1 session_id, клиент делает второй ход: с помощью обычной переадресации вида example.com/servletname?session_id=123456 тут же делает следующий запрос к тому же сервлету с этим параметром и получает в ответ Content-Type: application/octet-stream сотоварищи, что в конечном итоге приводит к появлению стандартного диалога в браузере. После этого документ удаляется из сессии, освобождая место.

    Несколько коротких замечаний:
    • Для всей AJAX-кухни мы используем MochiKit старой версии 1.3.1.
    • При выдаче документа с не-ASCII символами в имени файла это имя не отображается правильно во всех браузерах.
    • Для формирования JSON-ответов можно было бы воспользоваться готовыми библиотеками, например распространённой json-lib, но для такой малой задачи не хотелось добавлять к проекту новых зависимостей.


    Дополнительное описание работы можно получить в представленном коде.

    AbstractGetFileAJAXWay.java

    Абстрактный Java-класс, выполняющий основную работу. Конкретный класс-наследний должен реализовать в нём два метода, уникальных для каждого случая.

    package unchqua.getfileajaxway;

    import java.io.BufferedOutputStream;
    import java.io.IOException;
    import java.io.OutputStream;
    import java.io.Serializable;

    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;

    import java.util.Random;

    /**
    * Сервлет для получения документа с сервера.
    *
    * <p>Потомки этого класса должны реализовать следующие методы:</p>
    * <dl>
    * <dt>populateIdentity(HttpServletRequest)</dt>
    * <dd>Чтение из запроса данных, которые будут участвовать в поиске
    * документа.</dd>
    * <dt>getDocument(HttpServletRequest, Object)</dt>
    * <dd>Получение непосредственно документа на клиента.</dd>
    * </dl>
    *
    * <p>
    * Параметры: либо набор параметров для получения документа с EJB (поиск
    * на сервере по этим параметрам), либо параметр с именем
    * AbstractGetFileAJAXWay#DSID (идентификатор, полученный после первого запроса
    * сервера) для непосредственной выдачи документа клиенту.
    * </p>
    *
    * <p>Варианты JSON-ответа первого шага:</p>
    * <ul>
    * <li>Успешное получение документа с EJB:<br/>
    * <pre>{"result":"success","dsid":"362547383846347775"}</pre>
    * </li>
    * <li>Ошибка получения документа с EJB:<br/>
    * <pre>{"result":"failure","reason":"Нет связи с сервером!"}</pre>
    * </li>
    * </ul>
    */
    public abstract class AbstractGetFileAJAXWay extends HttpServlet {

    public static final String DSID = "dsid";

    public class GetFileAJAXWayException extends Exception {
    public GetFileAJAXWayException() { super(); }
    public GetFileAJAXWayException(String msg) { super(msg); }
    public GetFileAJAXWayException(Throwable thw) { super(thw); }
    public GetFileAJAXWayException(String msg, Throwable thw) { super(msg, thw); }
    }

    public interface IFileContainer extends Serializable {
    public String getFileName();
    public String getContentType();
    public long getFileLength();
    public byte[] getFileContent();
    }

    /**
    * Точка входа в сервлет.
    *
    * @param req HTTP-запрос.
    * @param resp HTTP-ответ.
    * @throws ServletException
    * @throws IOException
    */
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException {

    // Выдача документа клиенту.
    String dsid = req.getParameter(DSID);

    // Выдача документа клиенту.
    if (dsid != null && dsid.length() > 0) {
    try {
    deliverDocument(dsid, req, resp);
    } catch (GetFileAJAXWayException e) {
    e.printStackTrace();
    throw new ServletException(e);
    }
    }
    // Запрос документа с сервера.
    else if (req.getParameterMap().size() > 0) {
    try {
    Object identity = populateIdentity(req);
    retrieveDocument(identity, req, resp);
    } catch (GetFileAJAXWayException e) {
    e.printStackTrace();
    throw new ServletException(e);
    }
    }
    // Чёрти что.
    else {
    final String err = "Неизвестный режим работы!";
    log(err);
    sendFailureReply(err, resp);
    return;
    }
    }

    /**
    * Получение документа с сервера и сохранение его в сессии для последующего забора.
    * @param identity Объект с данными, которые участвуют в поиске документа. * @param req HTTP-запрос.
    * @param resp HTTP-ответ.
    * @throws ServletException
    * @throws IOException
    */
    private void retrieveDocument
    (Object identity, HttpServletRequest req, HttpServletResponse resp)
    throws IOException {

    // Сессия.
    HttpSession session = req.getSession(false);

    // Получение документа с помощью метода, реализованного в наследнике.
    IFileContainer cont;
    try {
    cont = getDocument(req, identity);
    } catch (Exception e) {
    final String err = "Ошибка получения документа с сервера: "
    + e.getMessage() + "!";
    log(err);
    sendFailureReply(err, resp);
    return;
    }

    // Уникальный идентификатор объекта.
    final String dsid = dsid(
    new long[]{ cont.hashCode(),
    cont.getFileLength(),
    session.hashCode() });

    // Сохранение документа в пользовательской сессии.
    session.setAttribute(dsid, cont);

    // Выдача клиенту сообщения о результате работы.
    sendSuccessReply(dsid, resp);
    }

    /**
    * Выдача ранее полученного документа клиенту.
    * @param dsid Идентификатор документа в сессии.
    * @param req HTTP-запрос.
    * @param resp HTTP-ответ.
    * @throws ServletException
    * @throws IOException
    */
    private void deliverDocument
    (String dsid, HttpServletRequest req, HttpServletResponse resp)
    throws GetFileAJAXWayException, IOException {

    // Сессия.
    HttpSession session = req.getSession(false);

    // Есть ли такой документ?
    Object sessobj = session.getAttribute(dsid);
    if (sessobj == null) {
    throw new GetFileAJAXWayException("Нет объекта \"" + DSID + "\" в сессии!");
    } else if (!(sessobj instanceof IFileContainer)) {
    throw new GetFileAJAXWayException("Неверный объект \"" + DSID + "\" в сессии!");
    }

    // Удаление документа из сессии.
    session.removeAttribute(dsid);

    // Документ.
    IFileContainer document = (IFileContainer) sessobj;

    // Выдача файла.
    resp.setStatus(HttpServletResponse.SC_OK);
    resp.setContentLength((int) document.getFileLength());
    resp.setContentType(document.getContentType());
    resp.setHeader("Content-Transfer-Encoding", "binary");
    /* // По стандарту -- в IE не работает
    String filename = "=?windows-1251?Q?" + new org.apache.commons.codec.net.QuotedPrintableCodec().encode(document.getFileName(), "Cp1251") + "?=";
    resp.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
    */
    /* // Обещали работу в IE -- фиг
    String filename = java.net.URLEncoder.encode(document.getFileName(), "Cp1251").replaceAll("\\+", " ");
    resp.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
    */
    /**/ // По-тупому
    String filename = document.getFileName();
    int dotpos = filename.lastIndexOf('.');
    if (dotpos > -1)
    filename = "file." + filename.substring(dotpos + 1);
    else
    filename = "file.dat";
    resp.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
    /**/
    OutputStream out = resp.getOutputStream();
    out.write(document.getFileContent());
    out.flush();
    out.close();
    }

    /**
    * Уникальный номер документа, положенного в сессию для последующего забора.
    *
    * @param trashheap Набор произвольных чисел для генерации случайного результата.
    * Может быть <tt>null</tt>, в этом случае в генерации не участвует.
    * @return Уникальный идентификатор документа в сессии.
    */
    private String dsid(long[] trashheap) {
    long dsid = System.currentTimeMillis();
    if (trashheap != null && trashheap.length > 0)
    for (int i = 0; i < trashheap.length; i++)
    dsid ^= trashheap[i];
    return Long.toString(Math.abs(new Random(dsid).nextLong()), 10);
    }

    /**
    * Экранирование символов в строках для присоединения их к строке формата JSON.
    * @param subject Исходная строка.
    * @return Результат.
    */
    private String escapeJSON(String subject) {
    if (subject == null || subject.length() == 0)
    return "";
    return subject.replaceAll("\"", "\\\"")
    .replaceAll("\\\\", "\\\\")
    .replaceAll("[\n\r]", "\\\\n");
    }

    /**
    * Формирование и отправка JSON-сообщения об успешном завершении работы.
    * @param dsid Идентификатор документа в сессии, который (документ) впоследствие можно забрать.
    * @param resp HTTP-ответ.
    * @throws ServletException
    * @throws IOException
    */
    private void sendSuccessReply(String dsid, HttpServletResponse resp)
    throws IOException {
    String dsidJSON = "{\"result\":\"success\",\"dsid\":\""
    + escapeJSON(dsid) + "\"}";

    sendAnyReply(dsidJSON, resp);
    }

    /**
    * Формирование и отправка JSON-сообщения об ошибке работы.
    * @param reason Строка ошибки.
    * @param resp HTTP-ответ.
    * @throws ServletException
    * @throws IOException
    */
    private void sendFailureReply(String reason, HttpServletResponse resp)
    throws IOException {
    String reasonJSON = "{\"result\":\"failure\",\"reason\":\""
    + escapeJSON(reason) + "\"}";

    sendAnyReply(reasonJSON, resp);
    }

    /**
    * Отправка сообщения клиенту.
    * @param json Отправляемая строка.
    * @param resp HTTP-ответ.
    * @throws IOException
    */
    private void sendAnyReply(String json, HttpServletResponse resp)
    throws IOException {

    final byte[] result_bytes = json.getBytes("UTF-8");
    final int CHUNK = 1024;
    final BufferedOutputStream output = new BufferedOutputStream(
    resp.getOutputStream(), CHUNK);

    resp.setStatus(HttpServletResponse.SC_OK);
    resp.setHeader("Content-Encoding", "UTF-8");
    resp.setContentType("text/plain; charset=UTF-8");
    resp.setContentLength(result_bytes.length);

    int bytes_pos = 0, bytes_chunk = 0;
    do {
    bytes_chunk = bytes_pos + CHUNK <= result_bytes.length
    ? CHUNK
    : result_bytes.length - bytes_pos;
    output.write(result_bytes, bytes_pos, bytes_chunk);
    bytes_pos += bytes_chunk;
    } while (bytes_pos < result_bytes.length);
    output.flush();
    output.close();
    }

    /**
    * Заполнение объекта необходимыми для поиска данными.
    * @param req HTTP-запрос.
    * @return Объект, который затем будет передан в {@link #getDocument(Object)}
    * для поиска документа.
    * @throws GetFileAJAXWayException Если в запросе недостаточно параметров
    * для поиска документа.
    */
    protected abstract Object populateIdentity(HttpServletRequest req)
    throws GetFileAJAXWayException;

    /**
    * Запрос документа с сервера, используя ранее созданный контейнер
    * с необходимыми для поиска данными.
    * @param req HTTP-запрос.
    * @param identity Параметры поиска документа на сервере.
    * @return Документ.
    * @throws GetFileAJAXWayException Невозможность возврата документа.
    */
    protected abstract IFileContainer getDocument(HttpServletRequest req,
    Object identity) throws GetFileAJAXWayException;

    }


    ConcreteDocumentRetrievalServlet.java

    Класс-наследник, реализующий требуемую для конкретного случая логику.

    package unchqua.getfileajaxway;

    public class ConcreteDocumentRetrievalServlet extends AbstractGetFileAJAXWay {

    public ConcreteDocumentRetrievalServlet() {
    super();
    }

    public Object populateIdentity(HttpServletRequest req)
    throws GetFileAJAXWayException {
    // Код чтения данных из запроса.
    return null;
    }

    public IFileContainer getDocument(HttpServletRequest req, Object identity)
    throws GetFileAJAXWayException {
    // Код получения файла из серверного хранилища.
    return null;
    }
    }


    GetFileAJAXWay.jsp

    Примерный JSP-файл, осуществляющий взаимодействие с сервлетом.

    <?xml version="1.0" encoding="UTF-8"?>

    <!DOCTYPE html
    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ru" lang="ru">
    <head>
    <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8"/>
    <meta http-equiv="Expires" content="Tue, Feb 07 1978 15:30:00 GMT"/>
    <meta http-equiv="Content-Style-Type" content="text/css"/>
    <meta http-equiv="Content-Script-Type" content="text/javascript"/>
    <title>GetFileAJAXWay example</title>

    <!-- MochiKit. -->
    <script type="text/javascript" src="/MochiKit-1.3.1/Base.js"></script>
    <script type="text/javascript" src="/MochiKit-1.3.1/Iter.js"></script>
    <script type="text/javascript" src="/MochiKit-1.3.1/DOM.js"></script>
    <script type="text/javascript" src="/MochiKit-1.3.1/Async.js"></script>

    <script type="text/javascript">
    <!--

    // Имя сервлета.
    var SERVLET_PATH = "/servletname"; // Логическое имя для класса unchqua.ConcreteDocumentRetrievalServlet .
    // URL для запроса.
    var SERVLET_URL = document.location.protocol + '//'
    + document.location.hostname
    + (document.location.port > 0 ? ':' + document.location.port : '')
    + SERVLET_PATH;

    /**
    * AJAX-запрос.
    */
    function JS_AJAX_GetElFAFile(reqid) {

    // Параметрыы.
    var parameters = {};
    parameters["reqid"] = reqid; // Например reqid – идентификатор файла в хранилище.
    parameters["rand"] = new Date().getTime(); // Чтобы обойти механизм браузерного кэширования.

    // Запрос.
    loadJSONDoc(SERVLET_URL, parameters)
    .addCallbacks(
    JS_AJAX_GetElFAFile_Success,
    JS_AJAX_GetElFAFile_Failure);
    }

    /**
    * AJAX-запрос завершился без ошибки.
    */
    function JS_AJAX_GetElFAFile_Success(jsondata) {

    // Это программная ошибка сервера?
    if (JS_AJAX_GetElFAFile_Is_response_error(jsondata)) {
    JS_AJAX_GetElFAFile_Failure(jsondata);
    return;
    }
    else if (typeof(jsondata.dsid) == "undefined") {
    JS_AJAX_GetElFAFile_Failure("Document is not received!");
    return;
    }

    // Выдача клиенту требуемого файла.
    window.location.href = SERVLET_URL + "?dsid=" + jsondata.dsid;

    }

    /**
    * AJAX-запрос завершился с ошибкой.
    */
    function JS_AJAX_GetElFAFile_Failure(jsondata) {

    var error_text =
    (typeof(jsondata.result) != "undefined"
    &amp;&amp; jsondata.result == "failure"
    &amp;&amp; typeof(jsondata.reason) != "undefined"
    &amp;&amp; jsondata.reason.length > 0)
    ? jsondata.reason
    : jsondata.message + " (" + jsondata.number + ")";

    window.alert(error_text);

    }

    /**
    * Is response error?
    *
    * jsonadata: JSON object just received.
    *
    * Returns flag (true/false).
    */
    function JS_AJAX_GetElFAFile_Is_response_error(jsondata) {

    // Ошибка программная (сгенерирована ПО сервера).
    var artifical_error = typeof(jsondata.result) != "undefined"
    &amp;&amp; jsondata.result == "failure";

    // Internal server error.
    var hard_error = typeof(jsondata.number) != "undefined"
    &amp;&amp; typeof(jsondata.message) != "undefined"
    &amp;&amp; jsondata.number == 500;

    return artifical_error || hard_error;

    }

    //-->
    </script>
    </head>
    <body>

    <a href="javascript:JS_AJAX_GetElFAFile(/*docid=*/123);">Дай мне этот файл!</a>

    </body>
    </html>

    Похожие публикации

    Средняя зарплата в IT

    110 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 8 355 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 1

      0
      Неплохо бы было подсветку синтаксиса сделать и оформить код в виде архива, чтобы удобно было скачивать.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое