TclコマンドをJavaで定義するには

 Tcl言語の拡張は普通C言語で行いますが、 Cの代わりにJavaを使うと、 どのOSに持っていってもコンパイル作業なしで使える、 オリジナルのTclコマンドを作ることができます。 ここではその簡単な例を紹介してみます。 この例はどこかで似たよーな例があるような気もしますが、 「max」という新しいTclコマンドを追加するものです。 maxの使い方は、例えば

% max 10.5 11.5 13.2 12.8 9.2
などと引数を指定すると、それらのうち一番大きな数(ここでは13.2) をコマンドの値として返すというものです。
% max
や
% max a i u e o
のような引数エラーの場合には、 catchコマンドでトラップできるエラーが発生するようにしています。

import tcl.lang.*;

class MaxCmd implements Command {
  public void cmdProc(Interp interp, TclObject argv[])
  throws TclException {
    double maxv = 0;
    // argv[0]はコマンドの名前なので、引数は[1]から
    if(argv.length < 2){
      throw new TclException(interp,
          "Ootto! too few arguments, should be:" +
           argv[0].toString() + " values...");
    }
    for(int i=1; i<argv.length; i++){
      // CのTcl_GetDouble同様、TclExceptionを発することがあります
      double v = TclDouble.get(interp, argv[i]);
      if(v > maxv) maxv = v;
    }
    TclObject result = TclDouble.newInstance(maxv);
    interp.setResult(result);
  }
}

public class MaxExtension extends Extension {
  public void init(Interp interp){
    interp.createCommand("max", new MaxCmd());
  }
}
// end.

 ここではひとつの拡張(extension)としてひとつのTclコマンド(max) だけを作っていますが、この拡張にあたるのが 上のMaxExtensionクラスで、TclコマンドにあたるのがMaxCmdクラスです。

 MaxExtensionは Extension のサブクラスとして定義し、 その抽象メソッドinitをオーバーライドして追加したいTclコマンドを定義しているJavaのクラス(ややこしいなあ) であるところのMaxCmdのインスタンスを interp.createCommandで登録しています。

 一方Tclコマンド「max」の実装をしている MaxCmd クラスはインターフェース Command を実装し、TclExceptionを放つクラスとして定義しないといけません。 このクラスは必ず上に示した形の抽象メソッドcmdProcを実装し、 その中で渡された引数 argv[] を使って処理をします。 値を返すには、 interp.setResultメソッドに、文字列かTclObjectのインスタンスを作って渡せばOKです。

 なおExtensionのサブクラスのほうは public にしておくのに注意が必要です。

 できたら、JDKのjavacか何かでコンパイルして、さっそく使ってみます。

package require java
java::load -classpath . MaxExtension
java::loadコマンドを上のように使えば、新しいTclコマンド「max」が追加されます。 で、このmaxコマンドを
set r [max 10.2 13.5 12.6 13.1 9.8]
のように使うと $r には「13.5」が入ると、こんな感じになります。 というわけで、 Cで拡張するのと大体同じ方法でJavaでもTcl/Tkを拡張することができるよー という紹介を少しだけしてみました。


Tcl BlendとJNDIによるユーザ認証

 それではせっかくなので、J2SEの機能を何か使って便利 (かどうかは分かりませんが)なTclコマンドを作ってみます。 ここで取り上げる例は、 JNDI(Java Naming and Directory Interface) というAPIを使って、ディレクトリ情報サーバに接続する例です。 接続して何をするかというと、LDAP (Lightweight Directory Access Protocol) によるパスワード認証を行います。 つまり、このTclコマンドは、ユーザIDとパスワードを渡すと、 LDAPサーバに問合せに行き、ユーザのパスワードが合っているかどうかを返す Tclコマンドです。こんなの今までなかった! …ンなわけありませんが、Tcl世界でLDAP認証を使おうとすると、 OpenLDAPに付属の寄贈ソフトウェアなどを使う必要がありますが、 文献やサンプルが得づらく、なかなか難しいです。

import javax.naming.*;
import javax.naming.directory.*;
import java.util.*;
import tcl.lang.*;

class LdapAuthCmd implements Command {
  private static final int AUTH_OK = 0;
  private static final int AUTH_NG = 1;

  public void cmdProc(Interp interp, TclObject argv[])
  throws TclException {
    if(argv.length != 8){
      throw new TclException(interp,
"wrong # of args, should be: ldapauth host port baseDN " +
"adminDN adminpass user passwird");
    }
    String host = argv[1].toString();
    int port = Integer.valueOf(argv[2].toString()).intValue();
    String baseDN = argv[3].toString();
    String adminDN = argv[4].toString();
    String adminPass = argv[5].toString();
    String userID = argv[6].toString();
    String userPass = argv[7].toString();

    int rc = checkAuth(host, port, baseDN, adminDN, adminPass,
               userID, userPass);
    interp.setResult(TclInteger.newInstance(rc));
  }

  public static int checkAuth(String host, int port, String baseDN,
 String appDN, String apppw, String userID, String usrpw) {
    if ((userID == null) || (userID.trim().length() == 0))
      return AUTH_NG;
    if ((usrpw == null) || (usrpw.trim().length() == 0))
      return AUTH_NG;
    //* LDAPサーバのホスト名とポート、
    //* 接続管理ユーザやパスワードなどの環境情報を設定します。
    Hashtable env = new Hashtable();
    env.put(Context.INITIAL_CONTEXT_FACTORY,
      "com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.PROVIDER_URL, "ldap://" + host + ":" + port + "/");
    env.put(Context.SECURITY_AUTHENTICATION, "simple");
    env.put(Context.SECURITY_PRINCIPAL, appDN);
    env.put(Context.SECURITY_CREDENTIALS, apppw);

    DirContext ctx;
    try { //* ここで例外が発生しなければ管理ユーザのログインはOKです
      ctx = new InitialDirContext(env);
    } catch (Exception e) {
      e.printStackTrace();
      return AUTH_NG;
    }

    //* ここで認証ユーザのIDをキーに、
    //* 「ユーザDN」とゆーものを取得します。
    SearchControls ctls = new SearchControls();
    ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
    String userDN = "";
    try {
      NamingEnumeration result
       = ctx.search(baseDN, "(uid=" + userID + ")", ctls);
      if ((result == null) || (!result.hasMore())) {
        return AUTH_NG; //* そんなユーザIDのユーザはいません。
      }
      SearchResult sr = (SearchResult) result.next();
      userDN = sr.getName();
    } catch (Exception e) {
      return AUTH_NG;
    }

    try { //* ここで一旦管理ユーザとしての接続を切ってしまいます
      ctx.close();
    } catch (Exception e) {
      e.printStackTrace();
      return AUTH_NG;
    }

    //* 今度は認証ユーザのDNとパスワードを接続環境情報に設定します。
    env.put(Context.SECURITY_PRINCIPAL, userDN + ", " + baseDN);
    env.put(Context.SECURITY_CREDENTIALS, usrpw);

    try { //* ここで例外が発生しなければ、認証OKです。
      ctx = new InitialDirContext(env);
      return AUTH_OK;
    } catch (Exception e) {
      return AUTH_NG; //* パスワード誤り
    }
  }
}

public class LdapAuthExtension extends Extension {
  public void init(Interp interp){
    interp.createCommand("ldapauth", new LdapAuthCmd());
  }
}
/* end. */

 もっとましな作り方もあったのかもしれませんが、 ココデ作るldapauthコマンドは豪勢にも7個の引数をとります。 これすなわち、
(1)LDAPサーバのホスト名またはアドレス、
(2)LDAPサーバのリスナーのポート番号、
(3)ディレクトリ検索の起点となるベースDN、
(4)接続する管理者ID、
(5)管理者IDのパスワード、
(6)認証ユーザのユーザID、
(7)認証ユーザのパスワード、
であります。 これだけの引数を指定したのに、コマンドの戻り値は 0か1だけ? そそそ、記号定数の通り、0なら成功、1なら失敗 (ユーザがいない+パスワード誤り+接続エラー)です。
 では試しにTcl Blendでこのコマンドを使ってみます。

package require java
java::load -classpath . LdapAuthExtension

set r [ldapauth "ldap.nsnhnkmmkk.co.jp" 389 "o=ALL-USERS" \
  "uid=adminuser,ou=applications,o=ALL-USERS" "adminpass" \
  "meister" "bokudame"]
puts "result = $r"

 このLDAP認証をTclから行う、というのは、 LDAPのC言語APIをラップしたTclライブラリも幾つか公開されているようですが、 ドキュメントなどのリソースがどれも不足しています。 ことこれに関してはJavaのJNDIを使うのが正解かもしれません。 というのは、 ユーザ認証にはセキュリティ上の暗号化通信技術との併用が不可欠なのですが、 Java2にはSSL(Secure Socket Layer)機構による暗号化通信のAPIも盛り込まれています。 今後両機能を統合しての展開、ということを考えれば、 Java実装を基盤にする意味もある程度アリと思いますが、 いかがでしたでしょうか。

拡張レビュー分室 top
(first uploaded 1999/03/04 last updated 2002/08/19, KOUKEN HEIJIMA)