最近、Perlの勉強をしていたので、yukiwikiminiをJavaに書き換えてみた。最初はServletでやろうとしたのだけど、Perlのようにリクエストごとにプロセスが分かれていないためグローバル変数に対応する変数を定義できない。そこで、別途Yukiwikimin.javaというクラスを作って、グローバルっぽいものはフィールドにした。こうすればリクエストごとにインスタンスを生成することでPerlっぽく書ける。
package yukiwikimini; import java.io.IOException; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * YukiWikiMini Version 1.0.2 をJava(GAEj)に移植したもの。 * * @author n-3104 */ public class Yukiwikimin { //my $dbname = 'ykwkmini'; // PerlのようにDBMファイルを利用できないので利用しなかった。 //my $thisurl = 'ykwkmini.cgi'; static private final String THIS_URL = "ykwkmini"; //my $frontpage = 'FrontPage'; static private final String FRONT_PAGE = "FrontPage"; //my $indexpage = 'Index'; static private final String INDEX_PAGE = "Index"; //my $errorpage = 'Error'; static private final String ERROR_PAGE = "Error"; //my $WikiName = '([A-Z][a-z]+([A-Z][a-z]+)+)'; static private final Pattern WIKI_NAME = Pattern.compile("([A-Z][a-z]+([A-Z][a-z]+)+)"); //my $kanjicode = 'sjis'; // 利用しなかった。普通のWebアプリだと指定がいるがGAEだといらないようなので。 //my $editchar = '?'; static private final String EDIT_CHAR = "?"; //my $bgcolor = 'white'; static private final String BG_COLOR = "white"; //my $contenttype = 'Content-type: text/html; charset=Shift_JIS'; static private final String CONTENT_TYPE = "text/html;charset=UTF-8"; //my $naviwrite = 'Write'; static private final String NAVI_WRITE = "Write"; //my $naviedit = 'Edit'; static private final String NAVI_EDIT = "Edit"; //my $naviindex = 'Index'; static private final String NAVI_INDEX = "Index"; //my $msgdeleted = ' is deleted.'; static private final String MSG_DELETED = " is deleted."; //my $cols = 80; static private final int COLS = 80; //my $rows = 20; static private final int ROWS = 20; //my $style = <<'EOD'; //<style type="text/css"> //<!-- //body { font-family: "Courier New", monospace; } //pre { line-height:130%; } //a { text-decoration: none } //a:hover { text-decoration: underline } //--> //</style> //EOD static private final String STYLE = "<style type=\"text/css\">" + "<!--" + "body { font-family: \"Courier New\", monospace; }" + "pre { line-height:130%; }" + "a { text-decoration: none }" + "a:hover { text-decoration: underline }" + "-->" + "</style>" ; //my %form; private Map<String, String> form; //my %database; private Database database; // Perlのようにグローバルにアクセスできるようにフィールドにしておく。 private HttpServletRequest req; private HttpServletResponse resp; public Yukiwikimin(HttpServletRequest req, HttpServletResponse resp) { this.req = req; this.resp = resp; } public void main() throws Exception { //&init_form; initForm(); //&sanitize_form; sanitizeForm(); //foreach (keys %form) { // if (/^($WikiName)$/) { // $form{mycmd} = 'read'; // $form{mypage} = $1; // last; // } //} // WIKI_NAMEのパターンに一致するパラメータが存在する場合は // そのページを参照するリクエストと判断する。 for (Enumeration<String> keys = req.getParameterNames(); keys.hasMoreElements();) { String key = keys.nextElement(); if (WIKI_NAME.matcher(key).matches()) { form.put("mycmd", "read"); form.put("mypage", key); } } //unless (dbmopen(%database, $dbname, 0666)) { // &print_error("(dbmopen)"); //} // DBMファイルのように操作できるようにDatabaseクラスを用意した。 database = new Database(); try { //$_ = $form{mycmd}; //if (/^read$/) { // &do_read; //} elsif (/^write$/) { // &do_write; //} elsif (/^edit$/) { // &do_edit; //} elsif (/^index$/) { // &do_index; //} else { // $form{mypage} = $frontpage; // &do_read; //} String mycmd = form.get("mycmd"); if ("read".equals(mycmd)) { doRead(); } else if ("write".equals(mycmd)) { doWrite(); } else if ("edit".equals(mycmd)) { doEdit(); } else if ("index".equals(mycmd)) { doIndex(); } else { form.put("mypage", FRONT_PAGE); doRead(); } } finally { //dbmclose(%database); database.close(); } } private void initForm() { //my ($query); //if ($ENV{REQUEST_METHOD} =~ /^post$/i) { // read(STDIN, $query, $ENV{CONTENT_LENGTH}); //} else { // $query = $ENV{'QUERY_STRING'}; //} // POSTとGETの差分を吸収。これはServletで行っているのでここでは何もしない。 //my @assocarray = split(/&/, $query); //foreach (@assocarray) { // my ($property, $value) = split /=/; // $value =~ tr/+/ /; // $value =~ s/%([A-Fa-f0-9][A-Fa-f0-9])/pack("C", hex($1))/eg; // &jcode'convert(\$value, $kanjicode); // $form{$property} = $value; //} // リクエストパラメータの抽出とデコード // デコードはGAEの場合は不要のようだ。 // ちなみにgetParameterMapで取得できるMapはUnmodifiableMapであるため利用できない。 // form = req.getParameterMap(); form = new HashMap<String, String>(); for (Enumeration<String> keys = req.getParameterNames(); keys.hasMoreElements();) { String key = keys.nextElement(); form.put(key, req.getParameter(key)); } } private void sanitizeForm() throws Exception { //if (defined($form{mypage}) and $form{mypage} !~ /^$WikiName$/) { // &print_error("(invalid mypage)"); //} // mypageパラメータがWIKI_NAMEに一致しない場合はエラーとする。 // mypageパラメータがない場合は参照モードであるためエラーとしない。 String mypage = form.get("mypage"); if (mypage != null && WIKI_NAME.matcher(mypage).find() == false) { printError("(invalid mypage)"); } } private void doIndex() throws IOException { //&print_header($indexpage, 0); //print qq|<ul>\n|; //foreach (sort keys %database) { // print qq|<li><a href="$thisurl?$_"><tt>$_</tt></a></li>\n| //} //print qq|</ul>\n|; //&print_footer; printHeader(INDEX_PAGE, false); for (String mypage : database.keySet()) { resp.getWriter().print("<li><a href=\"" + THIS_URL + "?" + mypage + "\"><tt>" + mypage + "</tt></a></li>\n"); } printFooter(); } private void doEdit() throws IOException { //&print_header($form{mypage}, 0); //my $mymsg = &escape($database{$form{mypage}}); //print <<"EOD"; //<form action="$thisurl" method="post"> // <input type="hidden" name="mycmd" value="write"> // <input type="hidden" name="mypage" value="$form{mypage}"> // <input type="submit" value="$naviwrite"><br /> // <textarea cols="$cols" rows="$rows" name="mymsg" wrap="off">$mymsg</textarea><br /> // <input type="submit" value="$naviwrite"> // </form> //EOD //&print_footer; printHeader(form.get("mypage"), false); String mymsg = escape(database.get(form.get("mypage"))); resp.getWriter().print( "<form action=\"" + THIS_URL + "\" method=\"post\">" + " <input type=\"hidden\" name=\"mycmd\" value=\"write\">" + " <input type=\"hidden\" name=\"mypage\" value=\"" + form.get("mypage") + "\">" + " <input type=\"submit\" value=\"" + NAVI_WRITE + "\"><br />" + " <textarea cols=\"" + COLS + "\" rows=\"" + ROWS + "\" name=\"mymsg\" wrap=\"off\">" + mymsg + "</textarea><br />" + " <input type=\"submit\" value=\"" + NAVI_WRITE + "\">" + "</form>" ); printFooter(); } private void doWrite() throws IOException { //if ($form{mymsg}) { // $database{$form{mypage}} = $form{mymsg}; // &print_header($form{mypage}, 1); // &print_content; //} else { // delete $database{$form{mypage}}; // &print_header($form{mypage} . $msgdeleted, 0); //} //&print_footer; if (form.get("mymsg") != null && form.get("mymsg").length() > 0) { database.put(form.get("mypage"), form.get("mymsg")); printHeader(form.get("mypage"), true); printContent(); } else { database.remove(form.get("mypage")); printHeader(form.get("mypage") + MSG_DELETED, false); } printFooter(); } private void doRead() throws IOException { //&print_header($form{mypage}, 1); //&print_content; //&print_footer; printHeader(form.get("mypage"), true); printContent(); printFooter(); } // ( // ((mailto|http|https|ftp):[\x21-\x7E]*) # Direct http://... // | // ($WikiName) # LocalLinkLikeThis // ) static private final Pattern LINK_PATTERN = Pattern.compile( "(" + "((mailto|http|https|ftp):[\\x21-\\x7E]*)" + // Direct http://... "|" + "(" + WIKI_NAME + ")" + // LocalLinkLikeThis ")" ); private void printContent() throws IOException { //$_ = &escape($database{$form{mypage}}); String mymsg = escape(database.get(form.get("mypage"))); //s! // ( // ((mailto|http|https|ftp):[\x21-\x7E]*) # Direct http://... // | // ($WikiName) # LocalLinkLikeThis // ) //! // &make_link($1) //!gex; // Javaではeオプション(置換後の評価)は出来ないためループで実装する。 Matcher m = LINK_PATTERN.matcher(mymsg); StringBuffer sb = new StringBuffer(); while (m.find()) { m.appendReplacement(sb, makeLink(m.group())); } m.appendTail(sb); mymsg = sb.toString(); //print "<pre>", $_, "</pre>"; resp.getWriter().print("<pre>" + mymsg + "</pre>"); } //if (/^(http|https|ftp):/) { static private final Pattern URL_PATTERN = Pattern.compile("^(http|https|ftp):"); private String makeLink(String s) { //$_ = shift; //if (/^(http|https|ftp):/) { // return qq|<a href="$_">$_</a>|; //} elsif (/^(mailto):(.*)/) { // return qq|<a href="$_">$2</a>|; //} elsif ($database{$_}) { // return qq|<a href="$thisurl?$_">$_</a>|; //} else { // return qq|$_<a href="$thisurl?mycmd=edit&mypage=$_">$editchar</a>|; //} if (URL_PATTERN.matcher(s).find()) { return "<a href=\""+ s + "\">" + s +"</a>"; } else if (s.startsWith("mailto:")) { // JavaではMatcherを利用しないとマッチ変数を利用できないためsubStringメソッドで代用した。 return "<a href=\""+ s + "\">" + s.substring("mailto:".length()) +"</a>"; } else if (database.containsKey(s)) { return "<a href=\""+ THIS_URL + "?" + s + "\">" + s +"</a>"; } else { return s + "<a href=\""+ THIS_URL + "?mycmd=edit&mypage=" + s + "\">" + EDIT_CHAR +"</a>"; } } private String escape(String s) { //my $s = shift; //$s =~ s|\r\n|\n|g; //$s =~ s|\r|\n|g; //$s =~ s|\&|&|g; //$s =~ s|<|<|g; //$s =~ s|>|>|g; //$s =~ s|"|"|g; //return $s; // Javaではundefを空文字と評価できないためnullは空文字とする。 if (s == null) return ""; s = s.replaceAll("\\r\\n", "\n"); s = s.replaceAll("\\r", "\n"); s = s.replaceAll("\\&", "&"); s = s.replaceAll("<", "<"); s = s.replaceAll(">", ">"); s = s.replaceAll("\"", """); return s; } private void printHeader(String title, boolean canEdit) throws IOException { //my ($title, $canedit) = @_; //print <<"EOD"; //$contenttype // //<html> // <head><title>$title</title>$style</head> // <body bgcolor="$bgcolor"> // <table width="100%" border="0"> // <tr valign="top"> // <td> // <h1>$title</h1> // </td> // <td align="right"> // <a href="$thisurl?$frontpage">$frontpage</a> | // @{[$canedit ? qq(<a href="$thisurl?mycmd=edit&mypage=$form{mypage}">$naviedit</a> | ) : '' ]} // <a href="$thisurl?mycmd=index">$naviindex</a> | // <a href="http://www.hyuki.com/yukiwiki/mini/">YukiWikiMini</a> // </td> // </tr> // </table> //EOD resp.setContentType(CONTENT_TYPE); // @{[$canedit ? qq(<a href="$thisurl?mycmd=edit&mypage=$form{mypage}">$naviedit</a> | ) : '' ]} // Javaでは式に三項演算子を含められないので事前に作成している。 String naviEdit = canEdit ? "<a href=\"" + THIS_URL + "?mycmd=edit&mypage=" + form.get("mypage") + "\">" + NAVI_EDIT + "</a> | " : ""; resp.getWriter().print( "<html>" + " <head><title>" + title + "</title>" + STYLE + "</head>" + " <body bgcolor=\"" + BG_COLOR + "\">" + " <table width=\"100%\" border=\"0\">" + " <tr valign=\"top\">" + " <td>" + " <h1>" + title + "</h1>" + " </td>" + " <td align=\"right\">" + " <a href=\"" + THIS_URL + "?" + FRONT_PAGE + "\">" + FRONT_PAGE + "</a> | " + naviEdit + " <a href=\"" + THIS_URL + "?mycmd=index\">" + NAVI_INDEX + "</a> | " + " <a href=\"http://www.hyuki.com/yukiwiki/mini/\">YukiWikiMini</a>" + " </td>" + " </tr>" + " </table>" ); } private void printFooter() throws IOException { //print "</body></html>"; resp.getWriter().print("</body></html>"); } private void printError(String msg) throws Exception { //my $msg = shift; //&print_header($errorpage, 0); //print "<h1>$msg</h1>"; //&print_footer; printHeader(ERROR_PAGE, false); resp.getWriter().print("<h1>" + msg+ "</h1>"); printFooter(); //exit(0); // Javaはリクエスト単位でプロセスを生成するわけではないためexitできない。 // ので、とりあえず例外にしておく。本当はServlet側で例外発生時にprintErrotとした方がよいだろうが。。 throw new Exception(msg); } }
極力、元のソースに対応するように書いてみた。ただ、以下の点はJavaでは同じように書けなかった。
- DBMファイル ⇒ Dababaseクラスを作成してそれっぽくしてみた。
- 正規表現置換での/eオプション ⇒ MatcherのappendReplacementメソッドを利用する。JavaDocにサンプルコードが載っている。
- String#matchesではマッチ変数を利用できない ⇒ Matcherが必要。面倒なのでsubStringメソッドで代用した。
- undefを空文字として評価してくれない ⇒ nullチェックを入れた。
- 式に三項演算子を含められない ⇒ 事前に処理をするようにした。
- exitできない ⇒ Servletはスレッド単位でかつプールしているので。よって例外をthrowするようにした。
- ヒアドキュメントがない ⇒ ひたすら文字列連結で書いた。
- 文字列中の変数評価 ⇒ 文字列連結演算子で代用。
まぁ、言語そのものの性質が異なるので当たり前のことなんだけど、Perl便利だな〜と思った部分もある。ちなみに、呼び出し側のServletのソースは以下の通り。単にリクエストごとにYukiwikiminを生成して呼び出しているだけ。
package yukiwikimini; import java.io.IOException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @SuppressWarnings("serial") public class YukiwikiminiServlet extends HttpServlet { public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { doPost(req, resp); } public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { try { new Yukiwikimin(req, resp).main(); } catch (Exception e) { e.printStackTrace(); } } }
ちなみにDatabaseクラスは以下の通り。PMFについてはGAEjのドキュメントを参照してください。http://code.google.com/intl/ja/appengine/docs/java/datastore/usingjdo.html#Getting_a_PersistenceManager_Instance
package yukiwikimini; import java.util.HashSet; import java.util.Set; import javax.jdo.Extent; import javax.jdo.JDOObjectNotFoundException; import javax.jdo.PersistenceManager; /** * Wikiのページを管理するデータベース。 * DBMファイル(Hash)と同じような感覚で操作できるようにした。 * 各WikiページはPageクラスとして表現している。 * * @author n-3104 */ public class Database { private PersistenceManager pm; public Database() { pm = PMF.get().getPersistenceManager(); } public void close() { pm.close(); } public Set<String> keySet() { Extent<Page> extent = pm.getExtent(Page.class, false); Set<String> set = new HashSet<String>(); for (Page p : extent) { set.add(p.getName()); } extent.closeAll(); return set; } public String get(String key) { try { Page page = pm.getObjectById(Page.class, key); return page.getMsg(); } catch (JDOObjectNotFoundException e) { return null; } } public void put(String key, String value) { // トランザクションなしで後勝ちとした。 pm.makePersistent(new Page(key, value)); } public void remove(String key) { try { Page page = pm.getObjectById(Page.class, key); pm.deletePersistent(page); } catch (JDOObjectNotFoundException e) { // 他のユーザーによって削除されている場合があるのでエラーとしない } } public boolean containsKey(String key) { try { Page page = pm.getObjectById(Page.class, key); return true; } catch (JDOObjectNotFoundException e) { return false; } } }
で、Pageクラスは以下の通り。TextをStringとして扱う方法はGAEでBlobやTextを定義する方法を参考にさせていただきました。
package yukiwikimini; import javax.jdo.annotations.IdentityType; import javax.jdo.annotations.PersistenceCapable; import javax.jdo.annotations.Persistent; import javax.jdo.annotations.PrimaryKey; import com.google.appengine.api.datastore.Text; @PersistenceCapable(identityType = IdentityType.APPLICATION) public class Page { @PrimaryKey private String name; @Persistent private Text msgText; public Page(String name, String msg) { this.name = name; this.msgText = new Text(msg); } public String getName() { return name; } public String getMsg() { if (msgText == null) { return null; } return msgText.getValue(); } }