MovableTypeを制覇する

Java だとこうなる

目次


XML-RPC ライブラリを使う

今からは Java の話だ。Java で投稿をする...というのをやるのだが、最終的な目的は筆者らしく、「James でモブログサーバを作る」というあたりに持っていく。要するに携帯電話からのメールを受け付けて、メール内容に応じて XML-RPC を発行し、ブログに投稿する...という奴である。とはいえ、その話題は実際には「James 君!」の方に譲る。ここではそれを実現するためのライブラリを作っちゃう、というだけだ。その方が「俺ぁ James なんて知らんし興味ないぞ」という人のタメでもあろう。

単に解凍すればOKである。すでに build/bloglib.jar が一緒になっているし、JavaDoc も入っている。細かい部分は JavaDoc とソースでご確認ください。依存ライブラリは

  1. xmlrpc-2.0.jar(XML-RPC 本体
  2. xerces-2.4.0.jar(XMLパーサ
  3. commons-codec-1.3.jar(Jakarta Commons の一つ。バイナリデータのデコードに要るようだ)

の3つだ。それぞれ Apache XML Project からででも落してくれ。

で筆者感覚では結構イイカゲンに作った...というのがここまでの Perl ベースのやり方なんだが、さすがに Java なので「再利用しやすい」ようにやってやろうではないか、という遠大な志を立てよう。で、Perl では低レベルで触ったが、Java ではもっとレベルの高いライブラリ XML-RPC ライブラリを使う。まあ、コイツの使いかたなんぞ、こんな程度のものだ。

import java.lang.ClassNotFoundException;
import java.io.IOException;

import org.apache.xmlrpc.XmlRpc;
import org.apache.xmlrpc.XmlRpcClient;
import org.apache.xmlrpc.XmlRpcException;

import java.util.Vector;
import java.net.MalformedURLException;

public class test {
    public static void main( String [] args ) throws SugXmlRpcException {
        XmlRpcClient client;
        Vector params = new Vector();
        String api = "mt.newPost";  // とか
        String driver = "org.apache.xerces.parsers.SAXParser";
        
        try {
            /* 結果の解読のためなどに SAX パーサを使う */
            XmlRpc.setDriver( driver );
        } catch( ClassNotFoundException e ) {
            throw new SugXmlRpcException( "driver " + driver +
                                          " cannot find in CLASSPATH", e );
        }
        try {
            client = new XmlRpcClient( url );
        } catch( MalformedURLException e ) {
            throw new SugXmlRpcException( "illegal URL: " + url, e );
        }

        /* Vector に与えたいデータをAPI定義の順番に追加する */
        params.add( .... );     
        params.add( .... );     
        .......

        try {
            Object o = client.execute( api, params );
            System.out.println( "result: " + o.toString() );
        } catch( XmlRpcException e ) {
            throw new SugXmlRpcException( "XML-RPC ERROR at execute", e );
        } catch( IOException e ) {
            throw new SugXmlRpcException( "I/O Error at execute", e );
        }
   }
}

まあ、レベルの高いライブラリなので、引数は java.util.Vector で順番にセットして渡し、戻り値はエラーでなければ Object 型が返る。だから適切にキャストして扱わないとダメだ。エラーであれば自動的に例外 XmlRpcException に変換してくれるので、適切にキャッチすればOKだ。

汎用XML-RPCクラスの作成

ふう、簡単だな。としてみると、これは「どう使うか?」の方に頭を使うべきだ。まあ、まずこの基本機能を基底クラスとして実装してみようか。

package jp.or.nurs.sug.mt;

import java.lang.ClassNotFoundException;
import java.io.IOException;

import org.apache.xmlrpc.XmlRpc;
import org.apache.xmlrpc.XmlRpcClient;
import org.apache.xmlrpc.XmlRpcException;

import java.util.Vector;
import java.net.MalformedURLException;

public class SugXmlRpc {
    private String url;
    private String api;
    private Vector params = new Vector();
    private XmlRpcClient client;
    private String driver = "org.apache.xerces.parsers.SAXParser";

    public SugXmlRpc() { }
    public SugXmlRpc( String url, String api ) throws SugXmlRpcException {
        this.url = url;
        this.api = api;
        try {
            XmlRpc.setDriver( driver );
        } catch( ClassNotFoundException e ) {
            throw new SugXmlRpcException( "driver " + driver +
                                          " cannot find in CLASSPATH", e );
        }
        try {
            client = new XmlRpcClient( url );
        } catch( MalformedURLException e ) {
            throw new SugXmlRpcException( "illegal URL: " + url, e );
        }
    }

    public void setUrl( String url ) { this.url = url; }
    public void setApi( String api ) { this.api = api; }

    public Object execute() throws SugXmlRpcException {
        try {
            return client.execute( api, params );
        } catch( XmlRpcException e ) {
            throw new SugXmlRpcException( "XML-RPC ERROR at execute", e );
        } catch( IOException e ) {
            throw new SugXmlRpcException( "I/O Error at execute", e );
        }
    }

    public void setParams( Object o ) {
        params.add( o );
    }
}

だからこの SugXmlRpc クラスを使うには、こうして使うわけだ。

   SugXmlRpc client = new SugXmlRpc( "http://www.nurs.or.jp/~sug/mt/mt-xmlrcp.cgi", 
                                     "metaWeblog.newPost" );
   client.setParams( "1" );   // blogId
   client.setParams( "sug" ); // username
   client.setParams( "hogehoge" ); // passwd
   client.setParams( content ); // item
   client.setParams( true );   // 再構築フラグ
   Object o = client.execute(); 
   if( o instanceof String ) {
       return (String)o;  // postId
   }

クライアントの作成

でまあ、そんな要領で Client クラスを各APIごとに作成しよう。現状で作ってあるのは、

PostClient
metaWeblog API に従って、投稿をする(metaWeblog.newPost)
RebuildClient
再構築をする(mt.publishPost)
CategoryClient
カテゴリを与える。ただしこの Client は複数のAPIを組み合わせて使いやすくしたものである(mt.getCategoryList, mt.setPostCategories, sug.createCategory)。
BloggerPostClient
blogger API に従って投稿する。これだとタイトルが与えられない(blogger.newPost)
InfoClient
現在利用可能なAPIのリストを取得する(mt.supportedMethods

の5つだ。まあ、単純な例として PostClient を掲げよう。

package jp.or.nurs.sug.mt;

import java.util.Vector;
import java.util.Hashtable;
import java.util.Date;

public class PostClient extends SugXmlRpc {
    private Hashtable struct;  // タイトル・本文・発行日などが入る
    private static String MT_NEWPOST = "metaWeblog.newPost";

    // 次の3つのプロパティはインスタンスで不変。コンストラクタでしか
    // セットできない
    private String user;
    private String pass;
    private String blogId;

    // 次の3つのプロパティはセッタがある。
    private String title;
    private String content;
    private boolean publish = false;  // 再構築デフォルトは false

    // コンストラクタ
    public PostClient( String url, String api ) throws SugXmlRpcException {
        super( url, api );
    }

    public PostClient( String url, String id, String user, String pass ) 
                                                    throws SugXmlRpcException {
        super( url, MT_NEWPOST );
        this.blogId = id;
        this.user = user;
        this.pass = pass;
    }

    // セッタ
    public void setTitle( String title ) {
        this.title = title;
    }

    public void setContent( String content ) {
        this.content = content;
    }

    public void setStruct( Hashtable struct ) {
        this.struct = struct;
    }

    public void setPublish( boolean publish ) {
        this.publish = publish;
    }

    // 投稿の実行
    public String post() throws SugXmlRpcException {
        setParams( blogId );
        setParams( user );
        setParams( pass );
        // タイトルがない場合には、本文からつくり出す
        if( title == null ) {
            if( content != null ) {
                int pos = content.indexOf( "\n" );
                if( pos == -1 ) {
                    title = content;
                } else {
                    title = content.substring( 0, pos );
                }
                if( title.length() > 20 ) {
                    title = title.substring( 0, 20 );
                }
            } else {
                throw new SugXmlRpcException( "Need to set content" );
            }
        }
        // 一括で setStruct() でセットすることもできる。
        // その場合セッタのない MovableType 固有のプロパティもセットできる
        if( struct == null ) {
            struct = new Hashtable();
            struct.put( "title", title );
            struct.put( "description", content );
            struct.put( "dateCreated", new Date() );
        }
        setParams( struct );
        setParams( publish );
        // 発行
        Object o = super.execute();
        if( o instanceof String ) {
            return (String)o;
        } else {
            throw new SugXmlRpcException( "XML-RPC returns " + 
                             o.getClass() + ":" + o.toString() );
        }
    }
}

ポイントはコンストラクタでしか与えることのできないプロパティと、セッタを用意したプロパティに分けたことかな。要するに Java の上でのインスタンスの範囲を決めておきたいわけである。まあ、これはサーバごと、ブログごとでそれぞれインスタンスを作るのが良かろう。だから、このシリーズでは、基本的に「url, blogId, user, pass」はコンストラクタでしかセットできず、インスタンスでは変更不可(Immutable)にしておこう。

で CategoryClient は「カテゴリがすでにあれば..」とかの処理がややこしいのでここでは API を表示するにとどめる。

public CategoryClient( String url, String id, String user, String pass ) throws SugXmlRpcException;
コンストラクタ。URL, blogId, user, passwd を渡して生成する。
public void setAllowCreate( boolean allow );
カテゴリの生成を許すかどうかのフラグ。これを呼んで明示的に許可しない限り、ないカテゴリを自動的に作成することはない。
public Vector getList() throws SugXmlRpcException;
現在 MovableType に存在するカテゴリのリスト。このメソッドは呼ばれるために XML-RPC を発行する「高価な」メソッドである。コンストラクタはその終りにこのメソッドを呼んでいるので、コンストラクタによる生成の時点でオブジェクトは、どんなカテゴリがあるか「知っている」。
public String getCategoryId( String cat );
カテゴリ名からIDを索引する。これは例の「番号:実名」の規則を知っているので、先頭の「番号:」は無視して一致する名称のカテゴリを探す。これは XML-RPC を呼ばないので、最新の情報かどうか気になるのならば、これを呼ぶ前に getList() を呼ぶべし。
public String createCategory( String categoryName, String parentId ) throws SugXmlRpcException;
public String createCategory( String categoryName ) throws SugXmlRpcException;
新規のカテゴリを生成する。直接これを呼んで悪いわけではないが、実際には「投稿する時に一緒に新規カテゴリを作る」という方のが一般的だろう。直接呼べば、生成すべきカテゴリの親カテゴリを指定できる(そうではないケースはすべてトップレベル・カテゴリー)。
public void executeSetCategory( String postid, String catid ) throws SugXmlRpcException;
投稿IDとカテゴリIDで投稿とカテゴリを結び付ける。内部利用を主に作ったが、公開していけないインターフェイスではない。
public void setCategory( String postid, String category ) throws SugXmlRpcException;
投稿IDとカテゴリ名で投稿とカテゴリを結び付ける。一番一般に利用されるインターフェイス。必要と設定(setAllowCreate)に応じて、自動でカテゴリを新規生成する。

投稿インターフェイスクラス(設計)

こんな調子でクライアント・クラスを作って行けばイイわけである。が...もう少しユースケース寄りに上位インターフェイスを考えた方がいいだろう。ここで考えなくてはならないことは、「モブログサーバを実現するんだと、どんなブログに XML-RPC を発行しなくてはいけないか判らん...」という問題だ。要するにブログは次の3つのカテゴリに分けて考えなくてはならない。

  1. bloggerAPI しか受け付けない
  2. metaWeblog API で定義されたものは受け付ける
  3. MovableType で独自拡張された API を受け付ける

なので、「投稿!」という単純なアクションを実現するに当たって、「どのレベルのAPIの使えるブログか?」というのを気にする...というのは避けたい。単純に、

package jp.or.nurs.sug.mt;

public interface BlogEntry {
    /* タイトルを保存できるか? */
    public boolean allowTitle();
    /* タイトルのセッタ */
    public void setTitle( String title );
    /* 本文のセッタ */
    public void setBody( String body );
    /* カテゴリを与えれるか */
    public boolean allowCategory();
    /* カテゴリのセッタ */
    public void setCategory( String category, boolean allow );
    /* 投稿。引数は再構築フラグ、戻り値は投稿ID */
    public String post(boolean publish) throws SugXmlRpcException;
    /* 再構築。実装無しでも可 */
    public void rebuild() throws SugXmlRpcException;
}

というインターフェイスをガイドラインとして、ストラテジで適切な実装クラスを差し替えるようにしてやりたいものである。なので、これを生成する「Factory」に当たるクラスはこんなところである。

package jp.or.nurs.sug.mt;

public class WeblogFactory {
    public static String MOVABLE_TYPE = "MovableType";
    public static String META_WEBLOG = "MetaWeblog";
    public static String BLOGGER = "Blogger";
    public static String AUTODETECT = "autodetect";

    public static BlogEntry getEntryForm( String type, String url,
                      String blogId, String user, String passwd )
                                                throws Exception {
        BlogEntry ret = null;
        if( MOVABLE_TYPE.equals(type) ) {
            ret = new MTBlogEntry( url, blogId, user, passwd );
        } else if( META_WEBLOG.equals(type) ) {
            ret = new MetaWeblogEntry( url, blogId, user, passwd );
        } else if( BLOGGER.equals(type) ) {
            ret = new BloggerEntry( url, blogId, user, passwd );
	} else if( AUTODETECT.equals(type) ) {
	    RSDFinder rsd = new RSDFinder( url );
	    return getEntryForm( rsd.getPreferredApiName(), rsd.getPreferredApiLink(),
				 rsd.getPreferredBlogId(), user, passwd );
        } else {
            /* もし何ならその他のケースは FQCN のクラス名だと思って、
               動的ローディングするのも手であろう。ここではそこまでしない。 */
            throw new NoWeblogDefinedException( "not defined WebLog as " + type );
        }
        return ret;
    }
}

これで、ブログの各タイプごとに適切なインスタンスが返ることになる。で、WeblogFactory.AUTODETECT というのは、お待ちかねの自動判定だ。これはちょっとマジックな機能なので、次ページで解説しよう。

だから、利用の場面では統一的に、

   BlogEntry entry = Weblog.getEntryForm( Weblog.MOVABLE_TYPE, url, id, user, pss );
   entry.setTitle( title );
   entry.setBody( body );
   entry.setCategory( category, true );
   entry.post( true );

で投稿ができるようになる。勿論、title, category はセットされない可能性があるわけだ。どうしようもない..というか、それが仕様なんである。

じゃあ、この BlogEntry の個々の実装クラスを作っていくことになる。まずは抽象基底クラス AbstractBlogEntry である。

package jp.or.nurs.sug.mt;

public abstract class AbstractBlogEntry implements BlogEntry {
    // インスタンスで変更不可
    protected String url;
    protected String blogId;
    protected String user;
    protected String passwd;

    // インスタンスで変更可
    protected String body;
    protected String title;
    protected String category = null;
    protected boolean allow = false;   // カテゴリ作成はしない

    public AbstractBlogEntry( String url, String blogId, String user, 
                                    String passwd ) throws SugXmlRpcException {
        this.url = url; this.blogId = blogId;
        this.user = user; this.passwd = passwd;
    }

    /* タイトル不可 */
    public boolean allowTitle() { return false; }
    public void setTitle( String title ) { this.title = title; }
    
    public void setBody( String body ) { this.body = body; }

    /* カテゴリ不可 */
    public boolean allowCategory() { return false; }
    public void setCategory( String category, boolean allow ) {
        this.category = category;
        this.allow = allow;
    }

    /* 抽象メソッド */
    public abstract String post( boolean publish ) throws SugXmlRpcException;

    /* 再構築はしない */
    public void rebuild() throws SugXmlRpcException {
        System.out.println( "This BlogEntry is NOT implemeted rebuild()!" );
    }
}

ここでは post() 以外のデフォルト実装を与えておく。rebuild() も警告出しだけの内容で一応作ってけばいい。デフォルトでは title も category もセットできないので、出来る実装クラスでは allowTitle(), allowCategory() を上書きして true を返すようにする。

投稿インターフェイスクラス(実装)

でこれの実装クラスとして、APIクラスに応じて、BloggerEntry, MetaWeblogEntry, MTBlogEntry の3つのクラスを作る。実際、BloggerEntry と MetaWeblogEntry クラスは大したものではない。

package jp.or.nurs.sug.mt;

import jp.or.nurs.sug.mt.PostClient;

public class MetaWeblogEntry extends AbstractBlogEntry {
    protected PostClient postClient;

    public MetaWeblogEntry( String url, String blogId, String user, 
                                    String passwd ) throws SugXmlRpcException {
        super( url, blogId, user, passwd );
        postClient = new PostClient( url, blogId, user, passwd );
    }

    public boolean allowTitle() { return true; }

    public String post( boolean publish ) throws SugXmlRpcException {
        postClient.setTitle( title );
        postClient.setContent( body );
        postClient.setPublish( publish ); // 再構築を一緒にするかどうか
        return postClient.post();
    }
}

とはいえ、MovableType 専用の MTBlogEntry はカテゴリ問題などがあるので、少々複雑になる。ここでは、mt.supportedMethod を使って「受け入れ可能なAPI」のリストを取るクライアントクラス InfoClient を使って、「ホントに *.createCategory があるか?」をあらかじめチェックして、それから動くようにしよう。まあ、実際には先ほど見たように「クラス名+メソッド名」の判定は MovableType の実装がイイカゲンなので、とりあえず「受け入れ可能なクラス名」+「受け入れ可能なメソッド名」で通れば何の問題もない。

package jp.or.nurs.sug.mt;

import java.util.Vector;

public class MTBlogEntry extends MetaWeblogEntry {
    // クライアント
    protected CategoryClient categoryClient;
    protected RebuildClient rebuildClient;
    protected InfoClient infoClient;

    // 再構築がまだの投稿
    private String waitPublish = null;
    // カテゴリを新規作成するAPIがあるかどうか
    private boolean hasCreateCategory = false;

    public MTBlogEntry( String url, String blogId, String user, String passwd )
                                                    throws SugXmlRpcException {
        super( url, blogId, user, passwd );
        hasCreateCategory = checkHasCreateCategory();
    }

    /* カテゴリ付き投稿ができる */
    public boolean allowCategory() { return true; }

    /* カテゴリを新規作成するAPIがあるかどうかチェック
      クラスを無視してやっているので、ひょっとして mt.createCategory
      がサポートされた時も動くかも。 */
    private boolean checkHasCreateCategory() throws SugXmlRpcException {
        infoClient = new InfoClient( url );
        Vector ret = infoClient.getMethods();
        for( int i = 0; i < ret.size(); i++ ) {
            String method = (String)ret.get(i);
            if( method.indexOf( "createCategory" ) >= 0 ) {
                return true;
            }
        }
        return false;
    }

    public String post( boolean publish ) throws SugXmlRpcException {
        String postid = null;
        if( category == null ) {
            // カテゴリがなければ簡単。
            postid = super.post( publish );
            if( publish == false ) {
                waitPublish = postid;
            } else {
                waitPublish = null;
            }
            return postid;
        }

        // カテゴリがある場合は順番がややこしい。
        // 1. まずカテゴリが存在するかどうかをチェック
        if( categoryClient == null ) {
            categoryClient = new CategoryClient( url, blogId, user, passwd );
        } else {
            categoryClient.getList();
        }
        String catid = categoryClient.getCategoryId( category );
        if( catid == null ) {
            // カテゴリがない
            if( allow && hasCreateCategory ) {
                // 作成を指定し、かつそのAPIがある
                categoryClient.setAllowCreate( true );
            } else {
                // なくて作成不可は投稿前に例外を投げる
                throw new SugXmlRpcException( "not have category " + category 
                                               + " and not allow create!" );
            }
        }
        
        // 2. 投稿
        postid = super.post( false );
        waitPublish = postid;
        
        // 3. カテゴリの付与
        categoryClient.setCategory( postid, category );
        
        // 4. 再構築(しないとカテゴリ付与が反映しない)
        if( publish == true ) {
            rebuild();
        }
        return postid;
    }

    /* 再構築すべき投稿の有無に応じ再構築 */
    public void rebuild( )  throws SugXmlRpcException {
        if( waitPublish != null ) {
            rebuildClient = new RebuildClient( url, waitPublish, user, passwd );
            rebuildClient.rebuild();
            waitPublish = null;
        }
    }
}

ふう、これで一応テストとかできる状態になっている。テストクラスとして、jp.or.nurs.sug.mt.TestMT を書いたので、適当に遊んでみて欲しい。

コンパイルで必要なのは「xmlrpc.*.jar」だけだが、これを実行するに当たっては、

  1. xmlrpc-2.0.jar(XML-RPC 本体)
  2. xerces-2.4.0.jar(XMLパーサ)
  3. commons-codec-1.3.jar(バイナリデータのデコードに要るようだ)

の3つのライブラリを必要とする。忘れずにゲット&クラスパスに含めてくれ。すべて Apache 配下なので、適当に探せば見つかるだろう。

で、これを使って「James と連携してモブログサーバを作る!」というのは、このシリーズよりも「James君!」のネタだ。そっちと一緒にすることにしよう。



copyright by K.Sugiura, 1996-2006