2013年5月2日木曜日

[Scala] PartialFunctionのリテラルって、caseから始めなくてもいいみたい

Scalaなのです。

もともと関数型言語が好きで、でもSI業界(のさらに自分がいるところ)はまだまだJavaだろうなー、でもやっぱ関数型言語使いたいなー、
と思ったので、Javaの資産が無駄なくご利用できるScalaをとりあえず勉強してみるか、と思ったのです。
これを実際に使うには、自分自身の働きかけだけではどうにもならない大いなる力がありますし、Scalaは結構変態仕様な部分もあるのでますますアレですが、
まあ関数型触っていて楽しいのでよし。

で、いきなりPartialFunctionの話です。
PartialFunctionがなんであるかは、ググればすぐにわかります。
ここ(ScalaのPartialFunctionが便利ですよ@ゆるよろ日記)とか。
しかし、それでは不親切すぎるので少しだけ。

「1引数をとる関数の一種なんだけど、引数の値によっては定義されていない(結果を返せない)ものがある関数」です。

プログラミング言語とはちょっと違いますが、
f(x) = 1 / x (x ≠ 0)
もPartialFunctionです。
xが0のところでは、定義されないでしょ。

たとえば上の関数をScalaで書いてみます。

val reciprocal = (x: Int) => 1 / x
println(reciprocal(0))
意味のある結果を返そうと思ったら、xをDoubleにするべきですが、ここは説明のため、Intのままで。

さて、こうして定義した"reciprocal"、0を代入すると、java.lang.ArithmeticException: / by zeroとなります。
0では割れませんエラー。お約束ですよね。

しかしこれではただの「場合によっては例外を投げる、普通の関数」です。

PartialFunctionには、isDefinedAtというメソッドがあります。
これがtrueになるところでは、定義されている、falseになるところでは定義されていない、という意味です。
そして、PartialFunctionのリテラルは、"case"から書き始める、つまり、パターンマッチから書き始めます。これはシンタックスシュガーだそうです。

val reciprocal: PartialFunction[Int, Int] = {
  case x if x != 0 => 1 / x
}
println(reciprocal.isDefinedAt(0))
println(reciprocal(0))
こうすると、パターンマッチに当てはまる値(つまり0以外)のところではisDefinedはtrue、そうでないところではfalseを返します。
上のコードでは、println(reciprocal.isDefinedAt(0))は"false"を表示します。
しかし、falseを返す値でもPartialFunctionは実行(apply)可能で、当然といえば当然ですが、3行目のコードはMatchErrorを投げます。

さて、こいつのつかいどころはどこか?

collectです。

collectは、Listなどのコレクションで使える、filterとmapが合わさったような機能。
PartialFunction pfを引数にとり、

list.withFilter(pf.isDefined).map(pf)
みたいなことをします。つまり、isDefinedでfalseを返す値を捨て、残りの値を変換してコレクションとして返す機能です。

ちょっと使ってみましょう。
Scalaのソースコードから1行コメントを抜き出し、TODOとFIXMEという文字列がついたものはちょっぴり強調する、という機能です。

object PFTest extends App {

  // 自分自身を読み込む
  val source = Source.fromFile("""このファイル自身のフルパス""")

  val todoPattern = "// TODO (.*)".r
  val fixmePattern = "// FIXME (.*)".r
  val commentPattern = "//(.*)".r

  // TODO このパターンを後で書き換えます
  source.getLines.map(_.trim()).collect {
    case todoPattern(contents) => "[ちゃんとやっとけよ]: " + contents
    case fixmePattern(contents) => "[ここほっといたらマジヤバイ]: " + contents
    // FIXME こんな文字列で大丈夫か?
    case commentPattern(contents) => "何かコメントがあったみたい: " + (if (contents.length() < 10) contents else contents.substring(0, 10))
  }.foreach(println)

  // クローズを忘れずに
  source.close()

}
ここでは、自分自身のソースコードを読み込んでいます。結果はこうなります。
何かコメントがあったみたい: 自分自身を読み込む
[ちゃんとやっとけよ]: このパターンを後で書き換えます
[ここほっといたらマジヤバイ]: こんな文字列で大丈夫か?
何かコメントがあったみたい: クローズを忘れずに
このコードでは、各行がどんな文字列か判別するため、正規表現のパターンマッチを使いました。

では、もっと簡単なケースを考えてみましょう。
1行コメントを抜き出して表示するだけでいい、となったら、どうでしょう。
もちろん、前のように正規表現を使ってもいいですし、withFilterとmapだけで簡潔に書けますが、こんな風にもかけるのです。

object PFTest extends App {

  // 自分自身を読み込む
  val source = Source.fromFile("""このファイル自身のフルパス""")

  // 1行コメントのみを取り出して、その内容を表示する
  source.getLines.map(_.trim()).withFilter(_.length() > 2).collect { line =>
    line.substring(0, 2) match {
      case "//" => line.substring(2)
    }
  }.foreach(println)

  // クローズを忘れずに
  source.close()
}
結果はこうなります。
自分自身を読み込む
1行コメントのみを取り出して、その内容を表示する
クローズを忘れずに
えー、お分かりになるでしょうか。
collectの引数はPartialFunctionですが、"case"から始まっていません。普通の関数リテラルっぽく始まりまり、最後にmatchを使っています。

普通の関数リテラルでも大丈夫なんじゃないの?と思って、matchじゃなくてifを使ったら、やっぱりコンパイルエラー。
どうやら、関数本体がmatchである場合のみ、PartialFunctionと認めてくれるようなのです。

いきなり"case"でなくてもよい、となれば、collectの使い勝手も良くなるというもの。
皆様、お試しあれ。

2012年11月22日木曜日

Struts2 + Spring + mybatis! とりあえず付箋ペタペタ機能を一通り作ってみた

超久しぶりのTagDiaryです。

ちょっと作るたびにblogを更新、だと却ってやる気を失ってしまうので、「ここでまずひと段落」というところまで作りました。
https://github.com/ymotchi/TagDiary

ここまでで、
  • ユーザを作る
  • ログインする
  • 付箋をペタペタ貼る
  • 付箋に文字を書く
  • 付箋を保存する
ができるようになりました。「TagDiary」ってくらいですから、日付ごとに付箋をまとめるような感じにしたかったのですが、まずはその前まで。

もし「利用してみたい」という方がいらっしゃれば、
mavenのプロジェクトを作る → プロジェクト直下にGitHubにあるsrcディレクトリとpom.xmlが来るようにダウンロードする → ビルドする → アプリケーションサーバを起動する
で使えると思われます。

また、こいつはMySQLも使っておりまして、テーブルを2つほど作ってやってください。

user
iduserINT(11) PK, NN
useridVARCHAR(16) NN, UQ
passwordVARCHAR(128) NN

tag
idtagINT(11) PK, NN
iduserVARCHAR(16) NN
tagseqINT(11) NN
xINT(11) NN
yINT(11) NN
widthINT(11) NN
heightINT(11) NN
contentsTEXT

わたくし、今までMySQLをほとんど知りませんで、"contents"列はCLOBにしたいんだけどCLOBは・・・などと思っておりましたら、MySQLではTEXTというのですね。

サーバが起動したら、「http://localhost:ポート/コンテキストルート/create_user_init」にアクセスしてユーザ作成、その後「.../login_init」にアクセスしてログインすると、付箋の編集画面に行きます。

ここで、四角の中をダブルクリックすると、付箋が出現。付箋はサイズ変更やドラッグ&ドロップOK。中身をクリックすれば、文字が書けます。付箋を右クリックすると、メニューが現れ、削除が可能です。下のボタンを押せば保存ができ、次回ログインしたときに、前回書いたものが再現されます。

・・・とつらつら書いてもわかりづらいものです。
よってここに、保存を除いた付箋ペタペタ機能のデモを作りました。この部分はJavascriptでいろいろやっているだけです。


なんかたまに右クリックメニューがあらぬところに現れますが、そこはご愛嬌。

エビデンス!エビデンス!!エビデンス!!! VBAでクリップボード監視 その3

第1回 → エビデンス!エビデンス!!エビデンス!!! VBAでクリップボード監視
第2回 → エビデンス!エビデンス!!エビデンス!!! VBAでクリップボード監視 その2
ユーザーフォームのソースコード → gist
標準モジュールのソースコード → gist

最後に補足。

このマクロを含めたブックは、自動保存をOFFにしたほうが良いです。また、マクロ実行中にブックを保存した場合、保存途中にPrintScreenを押してはいけません。良い子と僕らの約束だぞ。

というのも、クリップボードの変化を検知した後に、画像をシートに貼りつける部分
Public Sub pasteToSheet()
    Dim rowIdx As Integer
     
    With Sheet1
        If .Shapes.Count > 0 Then
            With .Shapes(.Shapes.Count)
                rowIdx = (.Top + .Height) / ROW_HEIGHT + 4
            End With
        Else
            rowIdx = 1
        End If
        .Cells(rowIdx, 1).PasteSpecial
    End With
End Sub
これの、"With .Shapes(.Shapes.Count)"がエラーになるから。
なんでかはよくわからないけど、保存中は、オートシェイプがVBAから正しく認識されないっぽい。
何せ保存中なのだから、そんなことがあってもしょうがない。というわけで、自動保存も非推奨です。

あと、前回こんな風に書きました。
  1.  WM_NCHITTESTであれば…
    これは実ははまるところなのですが、クリップボードの処理に直接関係ないので、次回に飛ばします。
これについてですが、WM_NCHITTESTの時にもCase Elseのようにもともとのウィンドウプロシージャを呼び出すようにすると、マウスカーソルがフォームの中に入った途端なぜだかCPUの使用率が跳ね上がり、フォームが反応しなくなります。無限ループに陥ってるっぽい。
勝手に元ネタにさせていただいたこちらでも、同じかわからないけど、似たような現象を確認されているし・・・。

この"WM_NCHITTEST"というのは、どうやら、マウスが動いたとか、ボタンが押されたとか、とにかくマウス系のイベントが起こると真っ先に送られるものらしく(MSのデベロッパー センター デスクトップ) 試しにこのメッセージを受け取った時にDebug.Printなどしてやると、フォームの上でマウスをぐりんぐりん動かしたときに、まあ出るわ出るわ。

この無限ループっぽい動きの原因はよく分かっていないのですが、このメッセージの時だけ何もしないようにしてやれば、とりあえず大丈夫そうです。
ただしこの場合、クリップボード監視中にフォームをクリックしても、ウィンドウがアクティブになった時の色にならなくなってしまうのですが・・・まぁ動くからよし。
チェックボックスのクリックもできるし。

というわけで、VBAでクリップボード監視はこれでおしまいです。

あと改良するなら、PrintScreenを押したときに、ちゃんと「押されたよ」的なポップアップがうにょんと最前面に出るといいかもしれません。対象のウィンドウを最大化していて、Excelが後ろに隠れていると、本当にPrintScreenが押されたかどうかすぐに確認できないんですよね。

2012年8月19日日曜日

エビデンス!エビデンス!!エビデンス!!! VBAでクリップボード監視 その2

前回 → エビデンス!エビデンス!!エビデンス!!! VBAでクリップボード監視
ユーザーフォームのソースコード → gist
標準モジュールのソースコード → gist

まずはユーザーフォームモジュールのコードから。
Private Sub CheckBox1_Change()
    If CheckBox1.Value = True Then
        Module1.catchClipboard
    Else
        Module1.releaseClipboard
    End If
End Sub

Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
    If CheckBox1.Value = True Then
        Module1.releaseClipboard
        CheckBox1.Value = False
    End If
End Sub
チェックボックスをチェックしたら、クリップボード監視(標準モジュールのcatchClipboardを実行)、 チェックを外したら、クリップボード監視終了(標準モジュールのreleaseClipboardを実行)。
チェックボックス外し忘れでフォームを閉じたときのための、念のためQueryCloseの時にもreleaseClipboardを実行。 以上。

では次に、標準モジュールのコード。
Option Explicit

Private Declare PtrSafe Function FindWindow Lib "user32.dll" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr
Private Declare PtrSafe Function SetWindowLong Lib "user32.dll" Alias "SetWindowLongA" (ByVal hWnd As LongPtr, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Private Declare PtrSafe Function CallWindowProc Lib "user32.dll" Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long, ByVal hWnd As LongPtr, ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As LongPtr) As LongPtr
Private Declare PtrSafe Function SendMessage Lib "user32.dll" Alias "SendMessageA" (ByVal hWnd As LongPtr, ByVal Msg As Long, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As LongPtr
Private Declare PtrSafe Function SetClipboardViewer Lib "user32.dll" (ByVal hWndNewViewer As LongPtr) As LongPtr
Private Declare PtrSafe Function ChangeClipboardChain Lib "user32.dll" (ByVal hWndRemove As LongPtr, ByVal hWndNewNext As LongPtr) As Long
Private Declare Function IsClipboardFormatAvailable Lib "user32.dll" (ByVal format As Long) As Long

Private Const GWL_WNDPROC As Long = -4

Private Const WM_DRAWCLIPBOARD As Long = &H308
Private Const WM_CHANGECBCHAIN As Long = &H30D
Private Const WM_NCHITTEST As Long = &H84

Private Const CF_BITMAP As Long = 2

Private Const ROW_HEIGHT As Double = 13.5

Private hWndForm As LongPtr
Private wpWindowProcOrg As Long
Private hWndNextViewer As LongPtr
Private firstFired As Boolean

Public Sub catchClipboard()
    hWndForm = FindWindow("ThunderDFrame", UserForm1.Caption)
    wpWindowProcOrg = SetWindowLong(hWndForm, GWL_WNDPROC, AddressOf WindowProc)
    firstFired = False
    hWndNextViewer = SetClipboardViewer(hWndForm)
End Sub

Public Sub releaseClipboard()
    Call ChangeClipboardChain(hWndForm, hWndNextViewer)
    Call SetWindowLong(hWndForm, GWL_WNDPROC, wpWindowProcOrg)
End Sub

Public Function WindowProc(ByVal hWnd As LongPtr, ByVal uMsg As Long, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As LongPtr
    Select Case uMsg
        Case WM_DRAWCLIPBOARD
            If Not firstFired Then
                firstFired = True
            ElseIf IsClipboardFormatAvailable(CF_BITMAP) <> 0 Then
                pasteToSheet
            End If
            If hWndNextViewer <> 0 Then
                Call SendMessage(hWndNextViewer, uMsg, wParam, lParam)
            End If
            WindowProc = 0
        Case WM_CHANGECBCHAIN
            If wParam = hWndNextViewer Then
                hWndNextViewer = lParam
            ElseIf hWndNextViewer <> 0 Then
                Call SendMessage(hWndNextViewer, uMsg, wParam, lParam)
            End If
            WindowProc = 0
        Case WM_NCHITTEST
            WindowProc = 0
        Case Else
            WindowProc = CallWindowProc(wpWindowProcOrg, hWndForm, uMsg, wParam, lParam)
    End Select
End Function

Public Sub pasteToSheet()
    Dim rowIdx As Integer
    
    With Sheet1
        If .Shapes.Count > 0 Then
            With .Shapes(.Shapes.Count)
                rowIdx = (.Top + .Height) / ROW_HEIGHT + 4
            End With
        Else
            rowIdx = 1
        End If
        .Cells(rowIdx, 1).PasteSpecial
    End With
End Sub
フォームから呼ばれる監視スタートのcatchClipboardは…
  1. FindWindowでフォームのハンドルを取得します。
    フォームのクラスは"ThunderDFrame"というらしいです。第2引数でフォームのキャプションを指定します。UserForm.Handleとかしたいのですが、少なくとも2010のVBAでは無理っぽいです。回りくどい。
  2. SetWindowLongでウィンドウプロシージャを自分で定義したWindowProcに入れ替える。
    この時の戻り値は、入れ替える前のウィンドウプロシージャのアドレスなので、記憶しておきます。
  3. firstFiredフラグをFalseにしておきます。
    この次の行でクリップボード監視を始めるのですが、SetClipboardViewerをした瞬間に、なぜか いきなり「クリップボードの中身が入れ替わった」イベント(後述するWindowProcにWM_DRAWCLIPBOARDが渡される)が発火してしま います。なので、それを抑えるために。
  4. SetClipboardViewerで、フォームをクリップボードビューアのチェーンにつなぎます。
    クリップボードのイベントを受け取るクリップボードビューアは、鎖のように連なっています。詳しくは後述。
    ここでの返り値は、自分の一つ後ろのクリップボードビューア(なければ0が返ります)なので、それを記憶しておきます。
一方、フォームから呼ばれる監視ストップのreleaseClipboardは…
  1. ChangeClipboardChainでフォームをクリップボードビューアのチェーンから切り離します。
    第1引数は切り離すウィンドウのハンドル、第2引数は切り離すウィンドウの一つ後ろのクリップボードビューアのハンドルです。
  2. SetWindowLongでフォームのウィンドウプロシージャを元に戻します。
    ちなみに、SetWindowLongを使っていますが、VBAでなくVBならば、SetWindowLongPtr(SetWindowLongPrtA)という関数が使えます。
    これは、第3引数と返り値がLongPtrになり、多い日も安心な感じがします。
    64bitを徹底的に気にするなら、本来はこちらを使うべきなのですが、user32.dllではなく、user32.libの関数のようで、だめでした。
    前回書いた通り、別にすべてのLongPtrをLongにしても大丈夫だと思います。きっと。
さて、catchClipboardにて、フォームのウィンドウプロシージャを"WindowProc"に入れ替えました。こういうのを、「サブクラス化」というらしいのですが、ではWindowProcを見てみます。
まず受け取ったメッセージuMsgを見てみます。
  1. WM_DRAWCLIPBOARDであれば…
    クリップボードの中身が変わったイベントです。このイベントはSetClipboardViewerを使った瞬間にもなぜか起きるので、そこはfirstFiredで調節しています。
    次 に、IsClipboardFormatAvailableで、中身がビットマップかどうかを判定しています。(Alt+)PrintScreenを使う と、ビットマップになります。テキストとかをコピペした時は無視させるためです。でビットマップならば、pasteToSheetでシートに貼りつけま す。
    最後に、次のクリップボードビューアが0でない(存在する)ならば、SendMessageでフォームが受け取ったメッセージを渡してやります。
    これは「クリップボードビューアは鎖のように連なっている」ためです。これをしないと、後続のクリップボードビューアが処理できません。見方を変えれば、後続に処理させたくない場合には、SendMessageを使わなければよいわけです。
    ここでは、シートに絵を貼りつけた後は別にクリップボードを独占する必要はないので、後続のビューアにメッセージを送ってやっています。
  2. WM_CHANGECBCHAINであれば…
    クリップボードビューアチェーンの誰かが離脱したイベントです。
    この時、OSで勝手にチェーンをつなぎかえてはくれません。自分でやります。
    wParamが離脱するビューア、lParamがその次のビューアのハンドルです。つまり、ChangeClipboardChainの引数そのものだったりします。
    なので、wParamが自分の一つ後ろならば、hWndNextViewerをlParamにしてやります。また、そうでないならば、自分は何もせずにSendMessageで後ろのビューアにメッセージを渡してやります。
  3.  WM_NCHITTESTであれば…
    これは実ははまるところなのですが、クリップボードの処理に直接関係ないので、次回に飛ばします。
  4.  それ以外であれば…
    フォームにやってくるメッセージは、クリップボード関連だけではありません。なんせ普通のフォームなので、マウスやキーなどのイベントのメッセージがやってきます(上のWM_NCHITTESTはマウス関連のイベントです)。
    そうした場合には、フォーム本来の処理をさせるべきです。
    そこで、CallWindowProcで、もともとフォームが持っていたウィンドウプロシージャを呼び出してやります。
最後に、pasteToSheetですが、これはまあコードを見ればOKかと思います。 Sheet1に次々とクリップボードの中身を貼りつけてやっています。

2012年6月17日日曜日

エビデンス!エビデンス!!エビデンス!!! VBAでクリップボード監視

ひとつとってはSEのため
ふたつとっては客のため
みっつとっては・・・誰のため?

さぁテストだ、となると、
ブラウザで画面表示 → PrintScreen → Excelに貼り付け
を延々と繰り返して、テストのエビデンスをとるわけですよ。

まあ非生産的なこと、誰の役に立つのでしょう、しかも表計算のExcelがお絵かき帳だなんて、と思っても口には出さないわけですが、
仕事で作んなくちゃいけない、と決まっているので作るわけです。

にしても、もう少しましにならないか・・・。

たとえば、PrintScreenを押すと、勝手にExcelに貼りつけてくれる、とか。
と思って、「VBA クリップボード 監視」で検索してみると・・・
やっぱり先人はいるのですね、たとえばここ
(興味を持たれた方は、(1)~(4)まであるので、全部読むことをお勧めします)

あとはこの仕組みに、ワークシートへの図のペーストを入れればオシマイ!なのですが、
ついでにちょっと気になるところがあったので、そこをちょっといじってみたりしました。

こんな感じです。適当なExcelブックを作成して、標準モジュールのModule1に書きます。

これは、64bit版のWindows 7でExcel 2010を使ったのでこういう書き方になったのですが、そうでなければ、たとえばExcel 2003とかなら、

LongPtr → Long
上のほうでDeclare FunctionしているところのPtrSafeを削除

とすればOKです。というか、別に2010でも同じようにやっても多分動くと思います。LongPtr使ってみたかっただけ。
そして、ユーザフォームを作成します。フォームにはチェックボックスをひとつだけ。


こんな感じです。

コードはこうします。ユーザフォームの名前は"UserForm1"、チェックボックスの名前は"CheckBox1"にします。また、ユーザフォームはモードレス、つまり、ShowModalをFalseにしておきます。

さて、説明は後回しにして、先に使い方を言いますと、

1. ユーザフォームを実行するなりして、 ユーザフォームを表示します。
2. フォームの中のチェックボックスをクリックします。
3. PrintScreenやAlt+PrintScreenするなどして、とにかくクリップボードに図を入れます。
4. すると、そのブックの1シート目に、クリップボードに入れた図がどんどん貼りついていくではないですか!

さて、次回は、このコードの解説と、自分的に気になったポイントを書こうと思います。

2012年3月7日水曜日

Struts2 + Spring + mybatis! 準備ができたところでGitHubに上げてみた

ここまでで、アプリを作成する準備段階は整ったと思うので、せっかくだからと、GitHubというものにあげてみることにした。

mavenで作ったこのプロジェクト全体をどうすればよろしくアップできるのか分からなかったので、とりあえずsrc下のみをあげることにした。
URLはこちら。
https://github.com/ymotchi/TagDiary

ただ、準備段階だからと言って、まったく動かないソースもアレだったので、トランザクション管理がちゃん動くかのテストも兼ねて、超簡単な画面をつけてみた。

まず、それ用のテーブル「testtable」を用意しておく。

testtable
idtesttableint(11) PK
testcolumnvarchar(32)

そしてTomcatなどのサーバでアプリを起動させ、「http://localhost:ポート/コンテキストルート/index」にアクセス。

で、「」の欄に

1) 適当な文字列を入れた場合
2) 「throwable」と入れた場合
3) 「exception」と入れた場合

で、挙動が変わってくる。

1)の場合は、『「(入力した文字列)」が入力されました』と表示され、testtableに入力した文字列が書き込まれる。
2)の場合は、エラーが画面に表示され、testtableに「throwable」が書き込まれる。
3)の場合は、エラーが画面に表示され、testtableに何も書き込まれない。

これは、applicationContext.xmlのトランザクションの設定のところで、
「rollback-for="java.lang.Exception"」という記述をしたため。
2)の場合は、サービスクラスのメソッドでThrowableが投げられるが、これはExcpetionの親クラスなので、ロールバックはしない。
一方3)の場合は、Exceptionが投げられるため、ロールバックされる。

この設定の効果が分かりやすく表れた。

細かいところは、実際にソースを見ていただくのが良いと思う。


忘れてた・・・。
このソースは、Java SE 7(以上)じゃないと動かない。
新機能を使ってみたくて、文字列のswitch文を使ってみたから。

2012年2月27日月曜日

Struts2 + Spring + mybatis! jspの最初の部分を1つのファイルにまとめる

web.xmlの中で、こんな風に書いた。
  <jsp-config>
    <jsp-property-group>
      <url-pattern>*.jsp</url-pattern>
      <el-ignored>false</el-ignored>
      <page-encoding>UTF-8</page-encoding>
      <scripting-invalid>false</scripting-invalid>
      <include-prelude>/WEB-INF/include/jsp_header.jsp</include-prelude>
    </jsp-property-group>
  </jsp-config>

ここで、"include-prelude"とは、すべてのJSPファイルの先頭に挿入したいコードを記述したファイルを指定する。pageやtaglib宣言なんかにうってつけ。
そこで、 このファイルをこのようにした。

jsp_header.jsp
<%@ page contentType="text/html; UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="f" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
 
コンテンツタイプをtext/html、出力文字コードをUTF-8にする宣言と、あとはよく使うJSTLとStruts2のタグのtaglib宣言。これで、各JSPファイルにいちいち書いて回る必要がなくなる。

ちなみに、すべてのJSPの末尾に特定のコードを挿入したい場合は、同じようなノリで"include-coda"を使う。
"prelude"に"coda"。このネーミングセンスはよいなぁと思う。