James君!〜高階Matcher

論理演算〜まずはパイプライン

さて、次の話題は論理演算だ。James の処理を書いていくと、「何かの Matcher の否定形が欲しいよね...」となることも多い。あるいは「Matcher A かつ Matcher B」とか「Matcher A もしくは Matcher B」といった処理がしたい....なんてことも考えるだろう。まあ、これをパイプラインで実現する、というのも手なんだが、これは意外にややこしい。たとえば、

<processor name="main">
  <mailet match="Matcher" class="処理" />
  .... 次の処理

を否定するならば、

<processor name="main" >
  <mailet match="否定されるべきMatcher" class="ToProcessor" >
    <processor>cond-yes</processor>
  </mailet>
  <mailet match="All" class="否定の場合にしたい処理" />
  <mailet match="All" class="ToProcessor" >
    <processor>main-next</processor>
  </mailet>
</processor>

<processor name="cond-yes" >
  <mailet match="All" class="ToProcessor" >
     <processor>main-next</processor>
  </mailet>
</processor>

<processor name="main-next">
   ....次の処理

となってややこしい。何か大昔にアセンブラで場合分け処理を JMP で書いたことを思い出していたりするぞ。

論理演算〜MailAttribute

なので少しこれはトンチで解決しよう。MailAttribute という便利なもんがあるじゃんか。なので、否定を MailAttribute で表現すればいい。

<processor name="main" >
  <-- 変数初期化 -->
  <mailet match="All" class="SetMailAttribute" >
     <result>false</result>
  </mailet>

  <-- 場合分けによる変数セット -->
  <mailet match="Matcher" class="SetMailAttribute" >
     <result>true</result>
  </mailet>

  <-- 判定 -->
  <mailet match="HasMailAttributeWithValue=result,false" 
       class="否定の場合にしたい処理" />

   ....次の処理

まあ、この調子で OR は簡単である。

<processor name="main">
  <-- 変数初期化(なくても動く) -->
  <mailet match="All" class="SetMailAttribute" >
     <result>false</result>
  </mailet>
  
  <-- 条件A -->
  <mailet match="MatcherA" class="SetMailAtribute" >
     <result>true</result>
  </mailet>

  <-- 条件B -->
  <mailet match="MatcherB" class="SetMailAtribute" >
     <result>true</result>
  </mailet>

  <-- 判定 -->
  <mailet match="HasMailAttributeWithValue=result,true" 
        class="OR条件の場合にしたい処理" />

   ....次の処理

だが、AND 条件はややこしい。要するにド・モルガン則で

A ∧ B = ¬(¬A ∨ ¬B)

ということをしないと素直に実現できないのである。

<processor name="main" >
  <!-- 初期化 -->
  <mailet match="All" class="SetMailAttribute" >
     <result>false</result>
  </mailet>

  <!-- 条件A側の処理 -->
  <!-- 条件A用変数の初期化 -->
  <mailet match="All" class="SetMailAttribute" >
     <result>false</result>
  </mailet>

  <mailet match="MatcherA" class="SetMailAttribute" >
     <resultA>true</resultA>
  </mailet>

  <mailet match="HasMailAttributeWithValue=resultA,false" class="SetAttribute" />
     <result>true</result>
  </mailet>

  <!-- 条件B側の処理 -->
  <!-- 条件B用変数の初期化 -->
  <mailet match="All" class="SetMailAttribute" >
     <result>false</result>
  </mailet>

  <mailet match="MatcherB" class="SetMailAttribute" >
     <resultB>true</resultB>
  </mailet>

  <mailet match="HasMailAttributeWithValue=resultB,false" class="SetAttribute" />
     <result>true</result>
  </mailet>

  <!-- ふう、やっとAND判定! -->
  <mailet match="HasMailAttributeWithValue=result,false" 
                 class="ANDで実行したい処理" />

厄介だね...これだとプロセッサを割りたくなっちゃうな...

Not Matcher の実装

でこの「厄介さ」の原因が何か?と考えてみると、結論はこうだ。

任意の Matcher について否定形を取れる Not Matcher がない。

ということだ。もし、Not Matcher があれば、AND 処理でも

<processor name="main">
  <mailet match="Not=MatcherA" class="SetMailAtribute" >
     <result>true</result>
  </mailet>

  <mailet match="Not=MatcherB" class="SetMailAtribute" >
     <result>true</result>
  </mailet>

  <mailet match="Not=HasMailAttributeWithValue=result,true" 
        class="AND条件の場合にしたい処理" />

と OR 条件並に書けるのである!

とはいえ、現状の Mailet には Matcher の中で別な Matcher を呼び出す例はない。 いろいろ研究してみると、意外にこれが出来たりするのである。ただし、ちょいとリフレクションなんかで汚いこともするが、それはご愛嬌だと思ってみて欲しい。

package jp.or.nurs.sug.james.matchers;

import org.apache.mailet.GenericMatcher;
import org.apache.mailet.Mail;
import org.apache.mailet.MailetContext;
import javax.mail.MessagingException;
import java.util.Collection;

import org.apache.avalon.framework.component.DefaultComponentManager;
import org.apache.avalon.framework.component.ComponentException;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;

import org.apache.mailet.Matcher;
import org.apache.james.transport.JamesSpoolManager;
import org.apache.james.transport.MatchLoader;
import org.apache.james.transport.Resources;

public class Not extends GenericMatcher {
    private Matcher matcher = null;  // 対象とする Matcher のインスタンス

    // init() はない
    public Collection match(Mail mail) throws MessagingException{
	if( matcher == null ) {
	    // もし matcher がなければ、サービスの中で作る
	    matcher = getMatcher();
	}
	// 元の recipients を退避しておく
	Collection org = mail.getRecipients();
	// Matcher の起動
	Collection ret = matcher.match( mail );
	// 結果に応じて反転
	if( ret == null || ret.isEmpty() ) {
	    return org;
	} else {
	    return null;
	}
    }

    // オプションで指定された Matcher を得る
    private Matcher getMatcher() throws MessagingException {
        // このスレッドの本体を得る
	Thread selfthread = Thread.currentThread();
	// new Thread( new JamesSpoolManager(...) ) で
	// SpoolManager のスレッド(Mailet実行スレッド)が動いている
	JamesSpoolManager jsm = (JamesSpoolManager)getField( selfthread, "target" );
	// MatchLoader は JamesSpoolManager の中で保存されているので、これを得る
	DefaultComponentManager compMgr = (DefaultComponentManager)getField( jsm, "compMgr" );
	if( compMgr == null ) {
	    throw new MessagingException( "cannot get Matcher!" );
	}
	try {
	    MatchLoader loader = (MatchLoader)compMgr.lookup( Resources.MATCH_LOADER );
	    // ターゲットとなる Matcher を生成して返す
	    return loader.getMatcher( getCondition(), getMailetContext() );
	} catch( ComponentException e ) {
	    throw new MessagingException( "cannot create matcher:" + getCondition(), e );
	}
    }
	
    // 汎用的なリフレクションメソッド
    private Object getField( Object target, String name ) {
	if( target == null ) return null;
	try {
	    Class clazz = target.getClass();
	    java.lang.reflect.Field [] fields;
	    while (clazz != null) {
		fields = clazz.getDeclaredFields();
		for (int index = 0; index < fields.length; index++) {
		    java.lang.reflect.Field field = fields[index];
		    if (field.getName().equals(name)) {
			field.setAccessible(true);
			return field.get( target );
		    }
		}
		clazz = clazz.getSuperclass();
	    }
	} catch( Exception e ) {
	    return null;
	}
	return null;
    }
}

まあ、リフレクションなぞ使うのはホントに邪道なんだが、同じ様な目に遭っている CommandListservManager Mailet でもリフレクションを使って必要な情報を引き出しているから、許してね。実際、個々の Matcher(や Mailet)のレベルだと、その呼び元に当たる JamesSpoolManager の情報を拾い出す手が他にないのである。要するに、親元のJames コンポーネントと、パイプラインを実行する JamesSpoolManager とは別々のスレッドで実行され、しかも相互の参照がないというかなり疎な結合になっている。フツーは

   ComponentManager mgr 
                   = (ComponentManager)getMailetContext().getAttribute( 
                                      Constants.AVALON_COMPONENT_MANAGER );
   JamesSpoolManager jsm
                  = (JamesSpoolManager)mgr.lookup( JamesSpoolManager.ROLE );

という具合で参照が出来るサブコンポーネントのはずだが、JamesSpoolManager はそうはなっていない。MailetContext() による James クラス経由の参照がうまく行かないのである。

とはいえ、Mailet/Matcher 実行のスレッド自体が、実は JamesSpoolManager である。だから、「match() とか service() を実行するスレッド」自体を拾い出せば、それが JamesSpoolManager なのである。まあ、当然 JamesSpoolManager は Runnable なので、ちょいとイケナいことをして currentThread() から、その Runnable を拾い出す...をして、JamesSpoolManager のインスタンスを取得している。

しかし、Matcher の init() の時点では、これが呼ばれるスレッドはメインスレッドなので、init() で準備をしておく、というわけにはいかない。あくまで「match() や service()が呼ばれるスレッド」が JamesSpoolManager なので、こういうコードになるわけだ。

JamesSpoolManager では、

org.apache.james.transport.JamesSpoolManager#initialize() 162行
        MailetLoader mailetLoader = new MailetLoader();
        MatchLoader matchLoader = new MatchLoader();
        try {
            mailetLoader.setLogger(getLogger());
            matchLoader.setLogger(getLogger());
            mailetLoader.contextualize(context);
            matchLoader.contextualize(context);
            mailetLoader.configure(conf.getChild("mailetpackages"));
            matchLoader.configure(conf.getChild("matcherpackages"));
            compMgr.put(Resources.MAILET_LOADER, mailetLoader);
            compMgr.put(Resources.MATCH_LOADER, matchLoader);
        } catch (ConfigurationException ce) {

のようにして、自身の compMgr に、あらかじめ用意した Mailet/Matcher のローダを保存しておいてくれている。だから、これを JamesSpoolManager インスタンスを更にリフレクトして取得してやる。要するに

    private DefaultComponentManager compMgr;

で compMgr を取得するインターフェイスがないのが、ツラいところなんである。

とはいえ、こういう風にやれば「高階Matcher」が作れてしまうのである。ホントにハックなものだが、一応これができるから、後は And だろうが Or だろうが、インターフェイスを決めさえすれば書けるわけである...が、リフレクションで private なフィールド名に依存してるコードなんてのは、邪道もいいとこなので、ここらへん今後のバージョンでもう少し考えて欲しいな。

論理演算Matcher

この要領で各種「論理演算Matcher」を書いて用意したが、まあ上記のやり方で十分である。ここでは仕様だけの解説とする。これらは全部、例の mailets.tgz に入っているので、インストールの仕方なんかはそっちを見てくれたまえ。一応、Recipient に関する問題がないわけではないので、動作がちょっと違うのは嫌だから、2系統用意してある。ぜひぜひ使ってくれたまえ。

jp.or.nurs.sug.james.matcher.Not

これは単に論理演算で否定を取る。「すべて」か「無」かの Not を取るから、受取人については「1件もなし」→「すべて」、「それ以外」→「null」で動作する。まあ、これは先ほど見た「Not Matcher の実装」で解説したものだ。

jp.or.nurs.sug.james.matcher.RecipientNot

これは Recipient 対応版である。言い替えると「対象とするMatcher」で選ばれなかった受取人だけを「Recipient」として返す。

jp.or.nurs.sug.james.matcher.And

複数の条件の「すべて」か「無」かの AND を取る。これは短縮評価をし、途中で戻り値が null になったら、そこで評価を打ち切って null を返す。書き方はこんな感じになる。

<mailet match="And=UserIs=sug,HostIsLocal" class="***" />

要するに「,」区切りで、対象とする Matcher をずらずらと並べればイイのだが、たまに「,」が有意な Matcher があるので、そういう場合には、

<mailet match="And=|SenderHostIs=outer.com,outer.co.jp|UserIs=sug|HostIsLocal" class="***" />

のように、先頭に「Javaシンボル先頭文字以外の文字」を追加してやる(この場合「|」)と、その文字が「区切り文字」として働く。

jp.or.nurs.sug.james.matcher.Or

複数の条件の「すべて」か「無」かの OR を取る。これは短縮評価をし、途中で戻り値が「元のRecipientそのまま」になったら、そこで評価を打ち切って「元のRecipient」を返す。書き方の要領は以下 And と同様である。

jp.or.nurs.sug.james.matcher.RecipientAnd

Recipient に関して And で絞りこむ Matcher である。これらは短縮評価をせずに、最後まで評価しつづける。書き方の要領は And, Or と違わない。

jp.or.nurs.sug.james.matcher.RecipientOr

Recipient に関して Or で併合していく Matcher である。これらは短縮評価をせずに、最後まで評価しつづける。書き方の要領は And, Or と違わない。

まあ、あと、

jp.or.nurs.sug.james.matcher.AbstractMetaMatcher
高階Matcher たちの抽象基底クラス
jp.or.nurs.sug.james.MetaLoader.java
Mailet,Matcher 内部から別な Matcher, Mailet を生成するためのユーティリティ

が用意してあるが、興味のある人は直接ソースを見てくれたまえ。



copyright by K.Sugiura, 1996-2006