正月休みの最終日、JSONパーサを作りました

コピペで使おう! ライブラリ」とか始めたものの現在内容がしょぼいので、正月休みの最終日である今日、JSONパーサを作りました。
朝から(といっても11:00頃ですが)からかかって20:00頃まで、途中昼食を作って食べたりおやつを食べたりとかはしたものの、割とがっつりコード書きして、現状時間切れでテストが足りないところはちょっとあるものの、以下くらいのJSONは読み込めるようになりました。

{
  "array": [1, 2.0, 123, 123.456, -1, -2.0],
  "array2": [3e5, 3E5, 3.1e5, 3.1E5, 3.1e+5, 3.1e-5, true, false, null],
  "string": "abcあいうえお",
  "string2": "\"\\\/\t",
  "string3": "\u3042",
  "string4": "\u3042\u3044\u3046",
  "string5": "\uD867\uDE3D",
  "string6": "\uD867\uDE3D\r\n",
  "string7": "\uD867\uDE3D\u3042\u3044\u3046",
  "objSub": {
    "sub1": 1,
    "sub2": "abc"
  },
  "objInArray": [
    {
      "objInArray1": 1,
      "objInArray2": "abc"
    }
  ],
  "arrayInArray": [
    [
      1,
      true,
      {
        "arrayInArray1": 1,
        "arrayInArray2": "abc"
      }
    ]
  ]
}

コードは例によってGitHubに上げてあります。
github.com
もうちょっとちゃんとテストしたら、「コピペで使おう! ライブラリ」にも上げます。

以前、同じような休日コーディング(Twitter実況付き。ほんのちょっとですが)で、samplanというプログラミング言語を作ったのですが、
kmaebashi.hatenablog.com
プログラミング言語といえば当然パーサを含むので、今回作ったものもまあ似たようなプログラムではあります。ただ、ちゃんとJSONに対応しようとすると「1.2e3」のような指数表記やら「\uXXXX」のようなunicodeエンコードやらにも対応しなければいけなくて、その辺は結構面倒でした……

今回のJSONパーサは、JSONを読み込んでJsonElementというインタフェースを返します。これのサブインタフェースにJsonObject、JsonArray, JsonValueがあり、それぞれがJSONのオブジェクト、配列、値を表します。つまり、これは「任意のJSONを読み込むためのJSONパーサ」であり、「あらかじめ決められた形式のJSONを読み込むためのJSONパーサ」ではありません。もちろんあらかじめ決められた形式のJSONを読んでもよいのですが、どうせ形式が決まっているなら、クラスにマッピングするところまでやりたいですよね。その辺はまた後日。

今回、1日でテストコード除いて785行(空行込み、コメントなぞ書いてない)ばかり書いてなかなかに疲れましたが、やっぱりコードを書くのは楽しいです。コードを書き始める前に、ティーポット1杯(カップ3杯分)の紅茶を淹れたのに1杯だけ飲んで残りは忘れてしまったりとか、トイレに行く間も惜しんでコード書いてたのでトイレ行くのはいつもぎりぎりだったりとか、ひさびさに集中したコード書きができました。

しかし疲れた。明日も休みになんないかな。

「コピペで使おう! ライブラリ」なるページを作ってみました

私のWebサイトkmaebashi.comに、「コピペで使おう! ライブラリ」というページを作ってみました。

kmaebashi.com

能書きはリンク先に散々書いてあるので、ここではあまり繰り返しませんが(つまり一部は繰り返すのですが)、プログラマのお仕事が、コードを読んだり書いたりではなく、「ググってライブラリを見つけ、ググって使い方を調べ、エラーが出たらググって解決策を探す」になってしまっているのは悲しいと私は思っています。

世間に山ほどあるプログラムでも、たとえ車輪の再発明でも、コードを書くのは楽しいですし、本人の力になると思っています。私は趣味で書いたプログラム(のうち他人でも使いそうなもの)をここに公開していきますので、読んだ方は、コピペして使うもよしいじりたおすもよし、あるいはこれを参考にまったく別のプログラムを作るもよし、ご自由にお使いください。

まあなんだ、既存のフレームワークやライブラリをぺたぺた組み合わせて動かすパズラーのような仕事じゃなくて、ガリガリとスクラッチからコードを書こうぜみんな。

現状の内容は以下の通り。

Java

  • CSVパーサ
    シンプルなCSVパーサです。
  • JDBCのPreparedStatementに名前付きパラメタ
    JDBCのPreparedStatementでは、名前付きパラメタが使えず「?」しか指定できないので、何番目の?かを数えなければいけません。名前付きのパラメタが使えたら、ソースが読みやすくなります。
    Spring FrameworkならNamedParameterJdbcTemplateなんてのが使えたりしますが、何もそれを使わなくても自作できますよ。
  • JDBCのResultSetからクラスにマッピング
    JDBCでDBを検索した検索結果ResultSetの内容をDTOとなるクラスに転記する機能です。DTOクラスの方には、DBの列と対応付けるためのアノテーションを付けておきます。

JavaScript

  • ※にマウスオーバーで脚注を表示する
    文中の「※2」とかにマウスオーバーすると、対応する脚注が読めるという機能です。はてなブログには最初からある機能ですが。そう、こんなの*1
  • <pre>要素内のコードブロックに行番号を表示する
    以前はkmaebashi.comではソースを貼るときは事前に(大昔に書いた)Cのプログラムで行番号を付与していましたが、その方法だとコピペ時に行番号が付いてきてしまうので。

*1:ほら、こんなの

ホームページのアクセスカウンタを作りました。2023年に。

うちのWebサイトkmaebashi.com、Niftyのメンバーズホームページに最初に開設したのが1998年、その後メンバーズホームページ閉鎖に伴い2004年にドメイン取ってレンタルサーバに引っ越して、その後もその会社が潰れて再販元に引き取られたりとか色々あったのですが、今年の10月末に、いよいよレンタルサーバ業者が事業を撤退するとのことで、さくらインターネットVPSを借りて全面的に移行しました。
その際、以前はレンタルサーバのものを借りていたアクセスカウンタも、スクラッチで作ったので今回はそのお話です。
大昔、個人の「ホームページ」が流行っていた頃は、「ホームページ」にはアクセスカウンタを付けるのがお約束のようなものでした。うちのサイトもその頃からあるので、ずっと継続してアクセスカウンタを付けています。外見としてはこんな感じ。

アクセスカウンタの画像
kmaebashi.comのアクセスカウンタ

(「あなたは○○人目のお客様です!」とか付けない程度の理性は当時からあったらしい)

この手のアクセスカウンタは、たいてい画像で表示します。そして、HTMLで画像を表示するには、img要素を使用します。kmaebashi.comの現行のアクセスカウンタでは、該当の箇所に以下のようにimg要素が入っています。

<img src="/accesscounter/show?counterid=kmaebashi" alt="アクセスカウンタ">

この「/accesscounter/show」に対するリクエストをJavaサーブレットで受けて、カウンタの画像を生成して返す、というのが、アクセスカウンタのプログラムがやっていることです。昔のアクセスカウンタは、Perlなんかで書いてあって、現在の閲覧数はテキストファイルなんかで持っていたりしましたが、今回はデータはPostgreSQLで保持するようにしました(掲示板等でどうせ使いますので)。
構成を図にするとこんな感じになります。

アクセスカウンタのシステム構成
アクセスカウンタのシステム構成

こうやって、img要素に対してのレスポンスで画像を返す、という方式にすると、サーバ側でプログラムが動かせず、ただのHTMLしかアップロードできないような昔ながらの「ホームページ」でも、外部のアクセスカウンタサービスを使ってアクセスカウンタを設置することができます。HTMLのimg要素は昔からドメインが違う別サーバの画像も取得できたからです。当時は、そうやって使うための「無料アクセスカウンタサービス」が多く公開されていました*1。昔ながらのアクセスカウンタがどれもこれも画像なのは、見た目をよくしたい、という目的以外にそういう理由があったのです。
アクセスカウンタのカウントアップの仕様自体もいろいろあって、同じブラウザで何度もF5を押しても増えないように、同一IPアドレスからの連続アクセスは無視するとか、そういうカウンタもありますが、今回私が作ったカウンタはその手の制御はしていません。なのでリロードするたびにカウンタがどんどん回ります。そのほうが嬉しい! ――確か当初、Niftyのメンバーズホームページで提供されていたアクセスカウンタはF5連打では増えなかったのですが、その後レンタルサーバに移ったときに移行したそのレンタルサーバのアクセスカウンタはF5連打で増えるタイプだったので、今回もそれを踏襲しました。やっぱりいっぱい増えた方が嬉しいですし。

その他、プログラムとかの細かい話は以下のページに書いておきます。
https://kmaebashi.com/programmer/accesscounter/index.html

せっかくアクセスカウンタを作ったことでもありますし、kmaebashi.com、今後ともたまには見てやってください。キリ番踏んだら掲示板に報告すること。踏み逃げ禁止!!

以下は広告です。2016年に出した本ですが、アクセスカウンタのような「レスポンスに画像を返すプログラムの作り方」含め、Webアプリケーションの基礎の基礎がわかる本と自負しております。
amazonアソシエイトプログラム、画像を含むリンクが貼れなくなっているようなので、自力で画像を上げました…

Webサーバを作りながら学ぶ 基礎からのWebアプリケーション開発入門
20231229235707

*1:今も探せばそこそこありますが、全盛期より数は減っていると思います……

サーブレットのweb.xmlで静的ファイルを除外する方法

たとえば生サーブレットで作ったオレオレフレームワークで、掲示板のひとつも作ろうと思ったとする。
オレオレフレームワークは、すべてのリクエストをひとつのサーブレットで受け付け、その先は独自のオレオレルータがルーティングして、各種処理を行って、HTMLを返す、ということをやりたいとする。
その場合、すべてのリクエストをOreOreFrameworkServletに渡せばいいんだな、ということでweb.xmlに以下のように書くと、

  <servlet>
    <servlet-name>OreOreFramework</servlet-name>
    <servlet-class>com.kmaebashi.framework.OreOreFrameworkServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>OreOreFramework</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

cssとかアイコン画像とかの静的ファイルへのリクエストまでOreOreFrameworkServletに流れてしまう。これは困る。

たとえば拡張子で判定して、*.cssだけ除外する、といった書き方は、web.xmlでは書けないらしい。気が利かない。
「"web.xml" "url-pattern"」でGoogle検索しようとしたら、「除外」ってサジェストが出たぐらいで、普通に需要はありそうなのだけど。

で、いろいろ検索すると、Stack overflowのページがいくつか見つかって、サーブレットフィルタを使えば行けるらしい……のだが。

https://stackoverflow.com/questions/8658949/in-a-web-xml-url-pattern-matcher-is-there-a-way-to-exclude-urlsstackoverflow.com

https://stackoverflow.com/questions/50056316/how-to-exclude-specific-path-from-web-xmlstackoverflow.com

サーブレットフィルタで、拡張子なりディレクトリなりで静的ファイルを判定した後どうすればよいか、書いてない。上のふたつの質問の解答はどちらも肝心のところが「// do something」とか「//do something else」になってる。しかもこのふたつの解答は、if文の条件判定がそれぞれ逆になっている。なんだこりゃ。

利用者から見えるパスに影響を与えてよいのなら、たとえば掲示板のアプリケーション名がbbsとして、一覧表示のURLを本来はこうしたいところ、
https://<ホスト>/bbs/list
サブディレクトリを挟んで
https://<ホスト>/bbs/app/list
とか、拡張子.doを付けて
https://<ホスト>/bbs/list.do
とかに変えれば、url-patternに"/app/*"や"*.do"を書いてそういうリクエストだけをOreOreFrameworkServletに渡せばよい。
その上で、webapps/bbs直下にbbs.cssを置けば、https://<ホスト>/bbs/bbs.cssCSSは取得できる。
でも、利用者から見えるところなのでやっぱり避けたい。/appとか入れるとCSSとかとの相対パスが狂うし、.doなんて懐かしのStrutsアプリみたいだ。

サーブレットフィルタは、サーブレットとかJSPとかの手前に挟まる。サーブレットコンテナ(Tomcatとか)が最初のサーブレットフィルタのdoFilter()を呼び出すので、次のフィルタとかサーブレットとかに処理を渡すなら、引数として与えられたchainのdoFilter()を呼べばよい。ではdoFilter()を呼ばなかったらどうなるかと言うと、そこでそのリクエストに対する処理は終わってしまう。拡張子が.cssでなければdoFilter()を呼んで(OreOreFrameworkServletに処理を渡して)、.cssだったら何もしない、というフィルタにすると、CSSは取得できない。戻り値か何かで「このurl-patternにマッチしなかったことにする」という指定ができればよいのだが、あいにくそんな機能はないし、doFilter()の戻り値はvoidだ。静的ファイルだと判定したら、自分でそのファイルを開いて、拡張子からContent-Typeを指定してファイルの中身をレスポンスに流し込めばよいのだろうが、さすがにそこまでやるのも嫌だ。

さらにいろいろ探して見つけたページがこちら。

https://stackoverflow.com/questions/13521946/how-to-prevent-static-resources-from-being-handled-by-front-controller-servlet-wstackoverflow.com

そこからリンクされているページ。内容は基本的に上記と同じ。
https://stackoverflow.com/questions/870150/how-to-access-static-resources-when-mapping-a-global-front-controller-servlet-on/3593513#3593513stackoverflow.com

結局、CSSなりを返したいと思ったら、フィルタではdoFilter()を呼べばよくて、逆にサーブレットに渡したい場合は/app/なり.doなりをくっつけてフォワードして、web.xmlのurl-patternにはそれをくっつけたのを書いておく、という方法でどうやらできた。下の例では、サーブレットに.doをくっつけているが、これは利用者には見えない(OreOreFrameworkServletには渡ってしまうが、まあ我慢する)。

web.xml

  <filter>
    <filter-name>staticresourcefilter</filter-name>
    <filter-class>com.kmaebashi.framework.StaticResourceFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>staticresourcefilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  <servlet>
    <servlet-name>OreOreFramework</servlet-name>
    <servlet-class>com.kmaebashi.framework.OreOreFrameworkServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>OreOreFramework</servlet-name>
    <url-pattern>*.do</url-pattern>
  </servlet-mapping>

StaticResourceFilter.java

import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;

public class StaticResourceFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest)request;
        String path = req.getRequestURI().substring(req.getContextPath().length());
        if (path.endsWith(".css") || path.endsWith(".html")) {
            chain.doFilter(request, response);
        } else {
            request.getRequestDispatcher(path + ".do").forward(request, response);
        }
    }
}

サーブレットなんて20年以上前からあるものだろうに、「url-patternに除外のルールを書く」くらいの機能、標準で付けておいてほしいよなあ。

PHPでCSVをダウンロードする方法(一時ファイルを作らない)

kmaebashi.comのサーバ移転に伴い、掲示板のデータも移行したわけですが、その際、PHP + MySQLで作った旧掲示板のデータをCSVにしてダウンロードする、という作業を行いました。
当方PHPは2004年だか2005年だかに旧掲示板を作って以来、PHPはほとんど使ってなくてすっかり忘れておりますが、PHPCSVをダウンロードするなんてよくある話だろうから、ちょっとGoogle検索すればサンプルが山ほど出てくるだろう、と思って検索してみたところ、サンプルは山ほど出てきたものの、どれもこれも「fputcsv()」というPHPの組み込み関数を使ってCSVを一時ファイルに書き出し、それをダウンロードさせている。いや別に一時ファイル作ってもよいのだけれど、こんなの作らなくてもできるだろストリームにダブルクオートだけ変換した文字列を吐くだけだろ、ということで、使わない形で作ったものをここに書き残しておきます。うちの掲示板のテーブル設計に依存したものなので、その辺は適当に読みかえてください。

  • 2005年から使っているものなのでDBへのアクセス方法が古いことはまあ大目に見てください。
  • boardidがtestbbsの投稿は、テスト掲示板への投稿で、広告だらけだったので除外しました。
  • DBの各列をいったんすべて変数に入れているのは――なぜだろう? 直接$row["serialid"]とか書いてもよさそうですが。数か月前の意図が思い出せない……
  • DBはEUCだったので、UTF8に変換してダウンロードしよう、と思ったのですが、「①」のような機種依存文字が化けるようで、結局EUCのままダウンロードしてクライアントのWindows用エディタ(xyzzy)でUTF8に変換しました。
<?php
require 'connect_db.php';

$sql_str = "select * from message where boardid <> 'testbbs' order by boardid, serialid";
$result = mysql_query($sql_str) or die('SQLエラー'.$sql_str);

header("Content-Type: application/octet-stream");
header('Content-Disposition: attachment; filename="bbs.csv"');

while ($row = mysql_fetch_assoc($result)) {
  $serialid=$row["serialid"];
  $boardid=$row["boardid"];
  $date = $row["posteddate"];
  $name = $row["name"];
  $mailaddress = $row["mailaddress"];
  $url = $row["url"];
  $subject = $row["subject"];
  $message = $row["message"];
  $altermessage = $row["altermessage"];
  $password = $row["password"];
  $salt = $row["salt"];
  $preformatted = $row["preformatted"];
  $deleted = $row["deleted"];
  $admindeleted = $row["admindeleted"];
  $parent = $row["parent"];
  $top = $row["top"];
  $ipaddress = $row["ipaddress"];
  $remotehost = $row["remotehost"];
  $useragent = $row["useragent"];

  echo $serialid . ",";
  echo $boardid . ",";
  echo $date . ",";
  echo quote_field($name). ",";
  echo quote_field($mailaddress). ",";
  echo quote_field($url). ",";
  echo quote_field($subject). ",";
  echo quote_field($message). ",";
  echo quote_field($altermessage). ",";
  echo quote_field($password). ",";
  echo quote_field($salt). ",";
  echo $preformatted. ",";
  echo $deleted. ",";
  echo $admindeleted. ",";
  echo $parent. ",";
  echo $top. ",";
  echo $ipaddress. ",";
  echo $remotehost. ",";
  echo $useragent. "\r\n";
}

function quote_field($src) {
//  $replaced = mb_convert_encoding($src, "utf-8", "euc-JP-win");
  $replaced = str_replace('"', '""', $src);

  return '"' . $replaced . '"';
}
?>

kmaebashi.comをSSL化しました

先日サーバ移転した私の個人サイト「K.Maebashi's home page」(kmaebashi.com)について、前々からやりたいと思っていたSSL化を実施しました。これにより、URLが

http://kmaebashi.com

から

https://kmaebashi.com

に変更になります。

今まで通り、http://kmaebashi.com~でアクセスしても、https://kmaebashi.com~に自動的に切り替わるように設定したので、閲覧者の皆様には特に影響ないかと思います。

SSL(Secure Sockets Layer)は通信の暗号化の技術です*1。もとより世界中に制限もなく公開しているWebサイトで、別にクレジットカード番号や個人情報を入力するところもないのに*2、なんで通信の暗号化をする必要があるのか、とも思わなくもないですが、一応うちのサイトではプログラムを載せたりしているので身元保証は必要でしょう。ドメイン認証の証明書なので「kmaebashi.comを運営している人は信用できる」と思っていただける方にしか意味はありませんが、送信元がkmaebashi.comであるということは保証できます。

こういう個人サイトでは、Let's Encryptという無料の証明書を使うのが定番のようです。これは無料はいいのですが3か月で期限が切れるので、普通はcertbotというツールを使って自動更新するように設定します(最初の導入にもcertbotを使います)。で、certbotをインストールするにはEPELリポジトリとかいうのを入れなければいけなくて、EPELリポジトリを入れた状態でdnfでインストールしようとしたらdnfがkilled.と言われて常に落ちるようになってしまった(certbotに限らず、何を入れようとしても、check-updateするだけでも落ちる)。慌てて戻して調べると、どうやらメモリ不足のようで、私が見つけたページでは512MBくらいのメモリのサーバを使っている人が「最低1GBは必要」とか書いていたけど要件が上がったのかうちの1GBのサーバでも動かないようだ。それなら一時的にサーバのメモリを2GBに上げて、インストールが終わったら戻すか、と思ったら、どうも一時的にサーバのメモリを上げるだけでも1年分の料金が取られるようで、それなら有償の証明書――年間980円のJPRSの証明書を買った方がマシだな、ということでそっちにしました。certbotで入れるより手間はかかりましたけれども。

今後ともよろしくお願いいたします。

*1:SSLは古い規格で、今実際に使われているのはTLS(Transport Layer Security)という技術ですが、「SSL化しました」とか言う時には今でもSSLと言うことが多いようなので、ここでもそう書いています

*2:一応掲示板には削除用のパスワードを設定できますが、こんな掲示板に、大事なパスワードを使い回す人はいないと信じたい。

kmaebashi.comのサーバ移転を行いました

私の個人Webサイトである「K.Maebashi's home page」(kmaebashi.com)を置いていたレンタルサーバ業者が事業を撤退するとのことで、新たにさくらインターネットVPSを借りてそちらに移転しました。

昨晩20:00頃にDNSの更新を行いました。TTLはデフォルトで3600秒だったので、DNSの浸透()は最長1時間で終わるはずで、今kmaebashi.comを見たら確実に新しい方のサーバが見えている、のではないかと思います。

http://kmaebashi.com

これが想定している新しいサーバを見たときの画面、

移転後サーバの画面

もし何らかの事情で古いサーバが見えてしまったとしたら、以下の画面になります。

古いサーバの画面

もし下の方の画面が見えてしまったら、掲示板なりここのコメント欄なりで教えてください(まあ、教えていただいたとして、私にできることはないように思いますが)。

このホームページは、最初に作ったのは1998年頃、私が使っていたプロバイダ(Nifty)のホームページスペースに置いたのが始まりです。その後、ドメインkmaebashi.comを取って移転して、そのレンタルサーバ業者が破産してサーバの再販元に引き取られたりとか、ところがその後もDNSはつぶれた会社に依存していて2017年頃に一度止まってしまったりとか、実はその後もそのDNSに依存していたらしいということが今回の移行で発覚したりとか色々ありましたが、なんやかんやでNiftyから数えれば25年にわたり運営してきたページです。今は見てくれる方も減ったとは思いますが、サーバがなくなるに任せて消えてしまう、というわけにはいきませんね。著書の正誤表なんかもここにありますし。

このページには、動的コンテンツとして、掲示板とアクセスカウンタがあります。掲示板は、元のものは「レンタルサーバでそれしか使えなかった」という理由でPHP + MySQLで書きましたが(2005年頃)、今回はVPSで言語も自由に選べるので、Java + Spring boot + PostgreSQLでゼロから作り直しました。データも移行済みです。この掲示板は、パスワードを設定しておけば自分の投稿を後から削除できるのですが、その削除用パスワードも「ほぼ」移行できたと思います。元の掲示板はパスワードにランダムなSALTを付けてMD5でハッシュしたものをDBに保持していて、そのままでいいかな、と思っていたら、ちょうど作り直している最中にMD5のパスワードが抜かれたというニュースがあったりして(この例ではSALTもついてなかったようですが)、MD5でハッシュ化したものに再度bcryptをかけるようにしました。「ほぼ」移行できたというのは、元のパスワードにシングルクォート等が入っていた場合、旧掲示板ではPHPのmagic_quotes機能で変換されてしまっていたからです。だからこんな変な機能を使ってはいけなかったんだ! (参考)

掲示板のURLは以下。拡張子が.phpになっていますが、これは過去投稿へのリンクを維持するためで、上述の通り中身はPHPではありません。

http://kmaebashi.com/bbs/list.php?boardid=kmaebashibbs

あとはアクセスカウンタ。これもJava + 生Servlet + PostgreSQLで書き直しました。私は「プログラムは依存が少ないほどえらい」と思っているのでさすがにこんなのにSpring bootとか使う気にはなれず。でもTomcatのコネクションプールは使った。

いまどき、「ホームページ」に「アクセスカウンタ」を、わざわざスクラッチで作り直してまで配置する奴もめずらしいかと思いますが、「元ページにあるものは基本全部引っ越す」方針としました。アクセスカウンタとか掲示板とかの作成記事はまた別途書こうと思います。

あまり更新もしていない過疎サイトですが、今後ともよろしくお願いいたします。