MovableTypeを制覇する

APIを自動判定しちゃう

目次
  1. RSDとは?
  2. RSDFinderの実装


RSDとは?

さて、前ページで「マジックな機能」として紹介したのが、「使えるAPIを自動判定する!」という強烈な機能だ。ただし、これはちょっと調査してみたが、ブログだからと言って「必ずある!」というものではない。まだまだ普及の途中のようだが、実に良い機能なので、ぜひぜひ紹介したい。

MovableType のインデックス・テンプレートの中に、「Atomフィード」や「RSS 1.0」などと並んで「RSD」というテンプレートがあるのは御存じか? これがマジックの根源である。このファイルはブログのトップページに atom.xml とか index.rdf なんかとどう様に作られる。中身はたとえばこんな感じだ。

<?xml version="1.0" ?> 
<rsd version="1.0" xmlns="http://archipelago.phrasewise.com/rsd">
  <service>
    <engineName>Movable Type 3.171-ja</engineName> 
    <engineLink>http://www.movabletype.org/</engineLink> 
    <homePageLink>http://www.nurs.or.jp/~sug/blog/</homePageLink> 
    <apis>
      <api name="MetaWeblog" preferred="true" 
         apiLink="http://www.nurs.or.jp/~sug/mt/mt-xmlrpc.cgi" blogID="1" /> 
      <api name="Blogger" preferred="false" 
         apiLink="http://www.nurs.or.jp/~sug/mt/mt-xmlrpc.cgi" blogID="1" /> 
    </apis>
  </service>
</rsd>

これが「このブログが受け入れ可能なXML-RPC API」を示していてくれているのである。これを見ると、

XML-RPC を受け付けるURLは...
apiLink って属性の「http://www.nurs.or.jp/~sug/mt/mt-xmlrpc.cgi」だ!
ブログIDは?
blogID 属性 で「1」だ!
で、APIは?
「MetaWeblog」と「Blogger」の両方がOKだ。ただし、「preferred="true"」とあるのは、「MetaWeblog」だから、こっちがオススメだ。

という具合に、「欲しかった情報」がぜーんぶ手に入る。勿論「MovableType API」が使えるかどうか、は「engineName」を見て「ホントに MovableType ?」を確認すればいい。

とはいえ...この rsd.xml は MovableType でこそ実装されている機能だが、他のブログでの普及はまだまだのようだ。だから「必ず使える!」というような代物ではない。「あったらラッキー!」くらいに捉えるのが現実的なようだ。しかし、今見たように「大変良い」機能である。だから、フライングしてこの機能を使っちゃおう。

で、更に、ブログのトップページには、

<link rel="EditURI" type="application/rsd+xml" 
  title="RSD" href="http://www.nurs.or.jp/~sug/blog/rsd.xml" />

のように RSS なんかと同様に埋め込まれる。だから、HTMLのページから rsd.xml を拾い出すことも出来てしまう。ここらへんの実装も考慮して、この RSD を扱うクラスを書いたわけである。

RSDFinderの実装

じゃあ、このような RSD の機構を使い倒すクラスを作る。どうせならば、コンストラクタを使いやすくして、

  1. ローカルファイルを指定して構築する
  2. ネットワーク経由でURLを指定して構築する
  3. HTMLの中から、
       <link rel="EditURI" type="application/rsd+xml" title="RSD" href="http://rsd.xmlのURL" />
    
    の行(面倒なので1行で書かれていることに限定)を抽出して、その中にある rsd.xml のURLにアクセスして構築する。

の何でもOKにしよう。どうせシグナチュアは単に、

    public RSDFinder( String url );

だけのことである。この引数 url は、上記の3パターンのどれでもOKだ。

で、複数の <api> がありうる現実を考慮すると、次のインターフェイスが適切だろう。

String getPreferredApiLink()
推奨される XML-RPC の受け入れURLを返す
String getPreferredApiName()
推奨されるAPIを返す。
String getPreferredBlogId()
推奨されるブログ識別子を返す。

つまり、preferred=true な api エレメントだけを抽出するインターフェイスである。これなら RSD の定義にこだわらず、一番適切なAPIを拾い出すことができる。で、getPreferredApiName() だが、ここでは MovableType がススンでることを考慮して、MovableType 3 以上ならば、特に「MovableType」という値を返すことにする。ちょいとイボな仕様だが、これはこれで現実的だろう。

package jp.or.nurs.sug.mt;

import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.AttributeList;
import org.xml.sax.HandlerBase;
import org.xml.sax.SAXException;

import java.net.URL;
import java.net.URLConnection;
import java.net.MalformedURLException;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.FileNotFoundException;

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

public class RSDFinder {
    private String engineName;  // ブログのアプリ名
    private String engineLink;  // ブログ開発元
    private String homePageLink;  // そのブログのトップページ
    private Vector apis;          // apis エレメント
    private String preferredApiName;  // 推奨API名
    private String preferredApiLink;  // 推奨XML-RPC の口
    private String preferredBlogId;   // 推奨ブログID

    /* デフォルトコンストラクタ */
    public RSDFinder() { }

    /* 普通に使うコンストラクタ。*/
    public RSDFinder( String url ) throws ParserConfigurationException, 
                                          SAXException, IOException {
	createFromURL( url );
    }

    /* URL からオブジェクトの実質的な内容を構築する。*/
    public void createFromURL( String url ) throws ParserConfigurationException,
                                                   SAXException, IOException {
	if( url.endsWith( ".xml" ) ) {
	    createFromXML( url );
	} else {
	    String rsd = getRSD( url );
	    createFromXML( rsd );
	}
    }


    /* XML からオブジェクトの実質的な内容を構築する。これは下請け的なメソッド
      である。引数 url は RSD の XML ファイル(のURL)でなくてはならない。
     */
    public void createFromXML( String url ) throws ParserConfigurationException,
                                                   SAXException, IOException {
        // JAXP 経由で SAX で解析
	SAXParserFactory factory = SAXParserFactory.newInstance();
	SAXParser parser = factory.newSAXParser();
	// 一時作業用の Hashtable
	final Hashtable work  = new Hashtable();
	// api エレメントのリスト
	apis = new Vector();
	// 解析
	parser.parse( url, new HandlerBase() {  // ハンドラ定義
		private String key = null;  // 着目エレメント名
		// エレメントの開始
		public void startElement( String name, AttributeList attr ) {
		    if( name.equals( "api" ) ) {
		        // api エレメントは属性のみが有効
			Hashtable api = new Hashtable();
			for( int i = 0; i < attr.getLength(); i++ ) {
			    String aname = attr.getName(i);
			    String avalue = attr.getValue(i);
			    api.put( aname, avalue );
			}
			// インスタンスに追加
			apis.add( api );
		    } else {
		        // 一時的なキーをセット
			key = name;
		    }
		}
		// エレメントの終了
		public void endElement (String name) {
		    // キーの消去
		    key = null;
		}
		// テキスト
		public void characters (char ch[], int start, int length) {
		    if( key == null ) { return; }
		    String str = new String( ch, start, length );
		    if( str != null ) {
		        // 一時的なオブジェクトに追加
			work.put( key, str );
		    }
		}
	    } );

	// 後始末
	if( apis.size() == 1 ) {
	    // api が1件しかなければ、それを推奨APIと考える
	    Hashtable at = (Hashtable)apis.get(0);
	    preferredApiName = (String)at.get( "name" );
	    preferredApiLink = (String)at.get( "apiLink" );
	    preferredBlogId = (String)at.get( "blogID" );
	} else {
	    // api が複数件あれば、その中から推奨を探す
	    for( int i = 0; i < apis.size(); i++ ) {
		Hashtable at = (Hashtable)apis.get(i);
		String preferred = (String)at.get( "preferred" );
		if( preferred != null && preferred.equals( "true" ) ) {
		    preferredApiName = (String)at.get( "name" );
		    preferredApiLink = (String)at.get( "apiLink" );
		    preferredBlogId = (String)at.get( "blogID" );
		}
	    }
	}

	// 一時作業用からインスタンス・フィールドに値をセット
	engineName = (String)work.get( "engineName" );
	engineLink = (String)work.get( "engineLink" );
	homePageLink = (String)work.get( "homePageLink" );
    }

    /* HTMLファイルの URL を指定して RSD ファイルのURLを抜き出す。
     下請け的に使われるメソッドである。
     制約: HTML ファイルには、次の行
     <link rel="EditURI" type="application/rsd+xml" title="RSD" href="http://rsd.xmlのURL" />
     があることを前提とする。これは独立した1行で、
     「<link rel="EditURI"」で始まることをマーカーとする。
     対象ファイルが単なる HTML である、ということしか保証がないので、
     これ以上高度な解析はしない。末尾は「>」で終っても「/>」で終っても
     どちらでもよい。
     */
    public String getRSD( String location ) throws IOException {
        // 接続
	URL url = new URL(location);
	URLConnection conn = url.openConnection();
	conn.setUseCaches( false );
	conn.connect();
	// URLの読み込み(HTMLを想定)
	BufferedReader br = new BufferedReader( 
                            new InputStreamReader( conn.getInputStream() ) );
	String line;
	String rsd = null;
	// <link rel=\"EditURI\" の検索
	while( (line = br.readLine()) != null ) {
	    if( line.startsWith( "<link rel=\"EditURI\"" ) ) {
		rsd = line;
		break;
	    }
	}
	if( rsd != null) {
	    // あれば href 属性値を抽出
	    int pos = rsd.indexOf( "href=\"" );
	    if( pos >= 0 ) {
		String href = rsd.substring( pos + 6 );
		pos = href.indexOf( "\"" );
		if( pos >= 0 ) {
		    href = href.substring( 0, pos );
		    return href;
		}
	    }
	}
	// 検索に失敗すれば FileNotFoundException を投げる
	throw new FileNotFoundException( "cannot find RSD link in " + location );
    }

    /* RSDの engineName エレメントを返す。*/
    public String getEngineName() {
	return engineName;
    }
    /* RSD の engineLink エレメントを返す。*/
    public String getEngineLink() {
	return engineLink;
    }
    /* RSD の homePageLink エレメントを返す。*/
    public String getHomePageLink() {
	return homePageLink;
    }
    /* RSD の apis エレメントを返す。積極的に使う意味はない。*/
    public Vector getApis() {
	return apis;
    }
    /* 推奨されるAPIを返す。
     * ただし、これは engineName が「Movable Type 3」で始まっていれば、
     * 「MovableType」を返す。そうでなければ、preferred な api名を返す。*/
    public String getPreferredApiName() {
	if( preferredApiName.equals( "MetaWeblog" ) &&
	    engineName.startsWith( "Movable Type 3" ) ) {
	    return "MovableType";
	} else {
	    return preferredApiName;
	}
    }
    /* 推奨される XML-RPC の受け入れURLを返す */
    public String getPreferredApiLink() {
	return preferredApiLink;
    }

    /* 推奨されるブログ識別子を返す。*/
    public String getPreferredBlogId() {
	return preferredBlogId;
    }
}

便利なので使い倒してくれ。ただし、WeblogFactory でやったように、何でもかんでも atuodetect でこれを呼び出すとオーバーヘッドがヒドイぞ。モブログサーバを作るんなら、登録のWebアプリでこれを使って XML-RPC に必要な諸元を取得して、DBに保存すべきだな。



copyright by K.Sugiura, 1996-2006