19 февраля 2007

Выбор имени ребёнка

15 февраля у меня родилась дочка, и все мои планы вырастить парня, не знающего Windows, накрылись медным тазом, но не в этом дело. Дочке нужно имя. Родители советуют назвать Еленой, но судя по какому-то найденному графику, это самое распространённое имя. Жена предлагает Анну или Марию. Мои варианты -- Татьяна и Светлана.

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

package ru.unchqua.namechooser;

import java.io.IOException;
import java.io.Writer;
import java.util.Date;
import java.util.Random;

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

public class NameChooserServlet extends HttpServlet {

private static final long serialVersionUID = 3558553303047697031L;

private static final int SECONDS = 3;

private static final String[] NAMES = {"Татьяна", "Светлана", "Елена", "Анна", "Мария"};

@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// Сессия.
HttpSession sess = req.getSession();

// Номер имени в массиве имён.
int nameidx = -1;

// Момент данного запуска.
long thisrun = new Date().getTime();

// Момент предыдущего запуска.
long lastrun = -1;
try {
lastrun = ((Long) sess.getAttribute("lastrun")).longValue();
} catch (Exception e) {
lastrun = new Date().getTime();
}

// Если запуск был более заданного числа секунд назад.
if ((thisrun - lastrun) > (SECONDS * 1000)) {
nameidx = 0;
log("Special case");
}
else {
Random rnd = new Random(sess.hashCode() ^ thisrun);
nameidx = rnd.nextInt(NAMES.length);
log("Random case");
}

// Вывод результата.
StringBuffer htmlbuf = new StringBuffer(1000)
.append("<?xml version=\"1.0\" encoding=\"KOI8-R\"?>");
.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">");
.append("<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"ru\" lang=\"ru\">");
.append("<head><title>Выбор имени</title></head>")
.append("<body>");
.append("<p style=\"text-align: center; margin-top: 5em; font-size: larger\">")
.append("<span style=\"font-weight: bold;\">")
.append(NAMES[nameidx])
.append("</span>")
.append("<br/>Евгеньевна</p>")
.append("</body></html>");
String html = htmlbuf.toString();
resp.setContentLength(html.getBytes("UTF-8").length);
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
Writer out = resp.getWriter();
out.write(html);
out.flush();
out.close();

// Сохранить время последнего запуска в сессии.
sess.setAttribute("lastrun", new Long(thisrun));
}

}


1 Если присмотритесь к коду внимательнее, то поймёте, какой страшный подвох я приготовил ничего не подозревающим родственникам.

16 февраля 2007

Retrieve document from server with proper error handling

For one of our intranet systems we implemented a simple search in files attached to official documents. Nothing difficult: search result is a list of files with its names and links to download them. Link is simple: servlet read a file by its id and outputs it with "application/octet-stream" or another MIME-type according to file's extension. But what if error occured at server? How can we tell operators about it? First option was to redirect them to a page with written error, but then they have to go back to list to try again, and this is not very comfortable.

We may request a servlet with AJAX XmlHttpRequest and output possible error with window.alert(), but then how to output document binary data from server in not so rare case of successful document retrieval? JavaScript's AJAX callback functions can't handle binary data well and can't present operator with standard browser's download dialog.

We ended up with this solution. Client requests servlet twice: one time it requests document (using AJAX with all its callback functionality), which is in turn requests real document from server and stores it in session with uniquely generated id, and another time client requests the same servlet, using document session id, which it had received at step 1. In case of failed server interchange (after step 1) operator is shown a message describing error just occured.

For AJAX interaction we use MochiKit v1.3.1, but are looking for lighter library.

Additional description may be found in GetFileAJAXWay servlet code.

GetFileAJAXWay.java:

package ru.unchqua.incubator.getfileajaxway;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
* Abstract servlet to get document from server AJAX way.
*
* Sequence is like this.
*
* Step 1. Client, using AJAX request, ask servlet to get particular document,
* identified by "docid". In response client receives JSON-message about
* successful or failed document retrieval; if job is done well, document itself
* is placed in session with unique identity. In that message client receives
* either "dsid" value (that session identity) or error string, which it can
* display to operator, using window.alert() for example.
*
* Step 2. On receiving "dsid" value client knows that document is ready to be
* requested, and servlet, called with that dsid, sends that document back to
* client, at the same time removing it from session, for it is not needed
* anymore.
*
* Descendants of this class must implement getDocument(long) method, which is
* used to actually receive needed document from somewhere.
*
* Calling parameters: either "docid" (identity of document needed) to retrieve
* it from server and place into session, or "dsid" (document session identity)
* for outputting that document to client.
*
* Examples of JSON messages after step 1:
*
* Successfull document retrieval:
* {"result":"success","dsid":"362547383846347775-ER8GERHGWER9GEH8-36644"}
*
* Retrieve error: {"result":"failure","reason":"No such document!"}
*/
public abstract class GetFileAJAXWay extends HttpServlet {

public GetFileAJAXWay() {
super();
}

/**
* Servlet entry point.
*
* @param req HTTP-request.
* @param resp HTTP-response.
* @throws ServletException
* @throws IOException
*/
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {

// There must be a session.
if (req.getSession(false) == null) {
final String err = "No session!";
sendFailureReply(err, resp);
log(err);
return;
}

// Request document from server.
String reqid = req.getParameter("reqid");

// Output document to client.
String dsid = req.getParameter("dsid");

// Request.
if (reqid != null && reqid.length() > 0) {
requestDocument(reqid, req, resp);
}
// Output.
else if (dsid != null && dsid.length() > 0) {
retrieveDocument(dsid, req, resp);
}
// God knows what.
else {
final String err = "Unknown mode!";
log(err);
sendFailureReply(err, resp);
return;
}
}

/**
* Getting document from server and storing it in session.
*
* @param docidstr Document id to get.
* @param req HTTP-request.
* @param resp HTTP-response.
* @throws ServletException
* @throws IOException
*/
private void requestDocument(String docidstr, HttpServletRequest req, HttpServletResponse resp) throws IOException {

long docid;

// Session.
HttpSession session = req.getSession(false);

// Document id.
try {
docid = Long.parseLong(docidstr);
if (docid <= 0) {
final String err = "Wrong document id: \"" + escapeJSON(docidstr) + "\"!";
log(err);
sendFailureReply(err, resp);
return;
}
} catch (Exception e) {
final String err = "Wrong \"docid\" parameter value: \"" + escapeJSON(docidstr) + "\", " + escapeJSON(e.getMessage()) + "!";
log(err);
sendFailureReply(err, resp);
return;
}

// Getting document from server.
GetFileAJAXWayByteContainer cont;
try {
cont = GetFileAJAXWayHelper.getDocument(docid);
} catch (Exception e) {
final String err = "Error while getting document: " + escapeJSON(e.getMessage()) + "!";
log(err);
sendFailureReply(err, resp);
return;
}

// Unique document identity to store it in session.
final String dsid = dsid() + "-" + session.getId() + "-" + docidstr;

// Store document in session.
session.setAttribute(dsid, cont);

// Notifying client on successful document retrieval.
sendSuccessReply(dsid, resp);
}

/**
* Sending document to client.
*
* @param dsid Document id to get it from session.
* @param req HTTP-request.
* @param resp HTTP-response.
* @throws ServletException
* @throws IOException
*/
private void retrieveDocument(String dsid, HttpServletRequest req, HttpServletResponse resp) throws IOException {

// Session.
HttpSession session = req.getSession(false);

// Do we have document requested?
Object sessobj = session.getAttribute(dsid);
if (sessobj == null) {
log("No \"" + dsid + "\" object in session!");
resp.setStatus(HttpServletResponse.SC_OK);
return;
} else if (!(sessobj instanceof GetFileAJAXWayByteContainer)) {
log("Wrong \"dsid\" object in session!");
resp.setStatus(HttpServletResponse.SC_OK);
return;
}

// Remove document from session to not pollute memory.
session.removeAttribute(dsid);

// Document.
GetFileAJAXWayByteContainer document = (GetFileAJAXWayByteContainer) sessobj;

// File output.
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentLength(document.getContent().length);
resp.setContentType("application/octet-stream");
resp.setHeader("Content-Transfer-Encoding", "binary");
resp.setHeader("Content-Disposition", "attachment; filename=\"" + dsid + "\"");
OutputStream out = resp.getOutputStream();
out.write(document.getContent());
out.flush();
out.close();
}

/**
* Unique document identity to store it in session.
*
* @return Unique id.
*/
private String dsid() {
return Long.toString(System.currentTimeMillis(), 10);
}

/**
* Escaping JSON-trouble symbols in string to append it to JSON response.
*
* @param subject Source string.
* @return Result.
*/
private String escapeJSON(String subject) {
if (subject == null || subject.length() == 0)
return "";
return subject.replaceAll("\"", "\\\\\"");
}

/**
* JSON response about successful job done.
*
* @param dsid Document id for client to get it later.
* @param resp HTTP-response.
* @throws ServletException
* @throws IOException
*/
protected void sendSuccessReply(String dsid, HttpServletResponse resp)
throws IOException {
String dsidJSON = "{\"result\":\"success\",\"dsid\":\"" + dsid + "\"}";

sendAnyReply(dsidJSON, resp);
}

/**
* JSON response about any failure.
*
* @param reason Error string.
* @param resp HTTP-response.
* @throws ServletException
* @throws IOException
*/
protected void sendFailureReply(String reason, HttpServletResponse resp)
throws IOException {
String reasonJSON = "{\"result\":\"failure\",\"reason\":\"" + escapeJSON(reason) + "\"}";

sendAnyReply(reasonJSON, resp);
}

/**
* Sending any JSON response to client.
*
* @param json JSON string to send.
* @param resp HTTP-response.
* @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();
}

/**
* Method to implement by subclasses.
*
* @param id Document id.
* @return Document body.
* @throws GetFileAJAXWayApplicationException
*/
protected abstract GetFileAJAXWayByteContainer getDocument(long id)
throws GetFileAJAXWayApplicationException;

}


ConcreteDocumentRetrievalServlet.java:

package ru.unchqua.incubator.getfileajaxway;

/**
* Concrete servlet.
*/
public class ConcreteDocumentRetrievalServlet extends GetFileAJAXWay {

private static final long serialVersionUID = 1L;

public ConcreteDocumentRetrievalServlet() {
super();
}

public GetFileAJAXWayByteContainer getDocument(long id)
throws GetFileAJAXWayApplicationException {
/**
* Code for actual document retrieval.
*/
return null;
}
}


GetFileAJAXWay.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">
<!--

// Servlet name.
var SERVLET_PATH = "/servlet/ru.unchqua.incubator.ajaxgetfile.ConcreteDocumentRetrievalServlet";
// URL to request.
var SERVLET_URL = document.location.protocol + '//'
+ document.location.hostname
+ (document.location.port > 0 ? ':' + document.location.port : '')
+ SERVLET_PATH;

/**
* Start AJAX request.
*/
function JS_AJAX_GetElFAFile(reqid) {

// Parameters.
var parameters = {};
parameters["reqid"] = reqid;
parameters["rand"] = new Date().getTime();

// Request.
loadJSONDoc(SERVLET_URL, parameters)
.addCallbacks(
JS_AJAX_GetElFAFile_Success,
JS_AJAX_GetElFAFile_Failure);
}

/**
* On successful AJAX call.
*/
function JS_AJAX_GetElFAFile_Success(jsondata) {

// Is that an error?
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;
}

// Request actual document.
window.location.href = SERVLET_URL + "?dsid=" + jsondata.dsid;

}

/**
* On failed AJAX call.
*/
function JS_AJAX_GetElFAFile_Failure(jsondata) {

var error_text =
(typeof(jsondata.result) != "undefined"
&& jsondata.result == "failure"
&& typeof(jsondata.reason) != "undefined"
&& 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) {

// Error is artifical (i.e. generated by hand at server).
var artifical_error = typeof(jsondata.result) != "undefined"
&& jsondata.result == "failure";

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

return artifical_error || hard_error;

}

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

<a href="javascript:JS_AJAX_GetElFAFile(/*docid=*/123);">Get me that document!</a>

</body>
</html>


Other classes like GetFileAJAXWayApplicationException, GetFileAJAXWayByteContainer and GetFileAJAXWayDocumentRetriever are self-explanatory and are not provided here.

15 февраля 2007

Генри Миллер, "Колосс Маруссийский"

<...>
Здесь я должен сделать отступление и поведать вам о моём друге Майо, художнике, которого я знавал в Париже. Настоящее его имя было Маллиаракис, и, думаю, родом он был с Крита. Как бы то ни было, когда мы входили в патракскую гавань, я понял наконец всё, что он пытался объяснить мне в тот вечер, и очень пожалел, что в этот момент его не было со мной, чтобы разделить моё восхищение. Я вспомнил, как он долго рассказывал мне о своей стране и под конец сказал со спокойной и твёрдой уверенностью: "Миллер, тебе понравится Греция, я в этом убеждён". Почему-то эти последние слова произвели на меня впечатление и больше, нежели весь его рассказ, врезались в память. Она тебе понравится... "Господи, она мне нравится!" — вновь и вновь мысленно повторял я, стоя у поручней и упиваясь плавным движением судна и поднявшейся суетой. Я откинулся назад и поднял голову. Никогда прежде я не видел такого неба. Такого изумительного неба. Я чувствовал себя совершенно оторванным от Европы. Я ступил в новое царство свободного человека — всё объединилось, чтобы произвести на меня впечатление небывалое и оплодотворяющее. Боже мой, я был счастлив! Но впервые в жизни, будучи счастлив, я полностью это осознавал. Замечательно, когда просто испытываешь обыкновенное счастье; ещё лучше, когда знаешь, что счастлив; но понимать, что ты счастлив, и знать, отчего и насколько, в силу какого стечения событий и обстоятельств, и всё же испытывать счастье, испытывать счастье от этого состояния и знания, — это больше, чем счастье, это блаженство, и если у тебя хватает ума, следует, не сходя с места, покончить с собой, остановить, так сказать, мгновение. Именно это со мной произошло — за исключением того, что мне не достало ни сил, ни мужества тут же и расстаться с жизнью. И хорошо, что я этого не сделал, потому что меня ждали мгновения ещё более восхитительные, нечто, чего не выразить даже и словом "блаженство", нечто такое, во что, попытайся бы кто описать мне, я вряд ли бы поверил.
<...>


Генри Миллер, "Колосс Маруссийский"