Pull to refresh

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

Reading time11 min
Views7.1K
Для одной из наших интранет-систем мы делали простой поиск по содержимым файлов, присоединённых к официальным документам. Результатом поиска был список имён файлов и ссылок на сервлет, эти файлы выгружающий. Сервлет читает файл по его идентификатору из хранилища и выдаёт его с «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>
Tags:
Hubs:
Total votes 4: ↑2 and ↓20
Comments1

Articles