お天気ウィジェットのソース

お天気ウィジェットのソースは以下の URL からダウンロードできます。


お天気ウィジェットのソース
http://www7b.biglobe.ne.jp/~fooami/weatherfx/src/src.html


お天気ウィジェットはプログラム的に大して難しい部分はありませんが(そんなこと言いつつ本人は所々でつまづいてましたが)、一番の核となる部分といえばやはり、リモートの Web サービスから天気予報を取得するところです。


Main.fx

...
function getWeatherForecast(city: String, day: String) {
    def url = "http://weather.livedoor.com/forecast/webservice/rest/v1?city...";
    send(url, XMLPullParser.processForecastResult);
}

function send(url: String, processResults: function (is: InputStream): Result) {

    alert("Getting feed from: ", url);
    try {
        var request = RequestHandler {
            location: url
            method: HttpRequest.GET
            processResults: processResults
        }
        request.enqueue();
    } catch (e:Exception) {
        e.printStackTrace();
        Main.error(Result {
                url: url;
                status: "\u4e88\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e...";
        });
    }
}
...

send 関数は、取得対象の地域が変更されたときや Timeline のスケジュールによって60分ごとに呼び出されます。引数に URL と関数(の参照)を受け取って、それをもとに RequestHandler インスタンスを生成して、enqueue() メソッドを呼び出します。


RequestHandler.fx

public class RequestHandler extends HttpRequest {

    public var processResults: function(is: InputStream): Result;

    public override var onException = function(exception: Exception) {
        exception.printStackTrace();
        Main.error(Result {
                url: location;
                status: "\u4e88\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e...";
        });
    }

    public override var onResponseCode = function(responseCode:Integer) {
        if (responseCode != 200) {
            Main.error(Result {
                    url: location;
                    status: "\\u4e88\u5831\u3092\u53d6\u5f97\u3067\u304d...";
            });
        }
    }

    public override var onInput = function(input: java.io.InputStream) {
        try {
            if (processResults != null) {
                var result = processResults(input);
                Main.complete(result);
            }
        } finally {
            input.close();
        }
    }
}

RequestHandler クラスは javafx.io.http.HttpRequest クラスのサブクラスで、onException、onResponseCode、onInput をオーバーライドしています。onException メソッドは例外発生時に呼び出されるコールバックで、エラー処理をしています。onResponseCode メソッドはサーバーからレスポンスが到達してレスポンスコードを取得したときに呼び出されるコールバックで、レスポンスコードが200でないときに、やはりエラー処理をしています。onInput メソッドはサーバーからデーターを受け取ったときに呼び出されるコールバックで、send 関数の中で設定された processResults メソッドを、引数の InputStream を引数に渡して呼び出し、結果を Main クラスのcomplete 関数に渡して呼び出て完了を通知します。


XMLPullParser.fx

...
public function processForecastResult(is: InputStream): Result {
    var parser = ForecastResponseParser {
        documentType: PullParser.XML;
        input: is;
    };
    parser.parse();
    is.close();
    parser.result;
}
...

RequestHandler の onInput の中で呼び出している processResults メソッドの実体は XMLPullParser の processForecastResult 関数です。この関数の中では ForecastResponseParser のインスタンスを生成して、parse() メソッドを呼び出しています。ForecastResponseParser が InputStream からデータを読み出して、結果を解析します。


XMLPullParser.fx

...
abstract class ResponseParser extends PullParser {
    public override var onEvent = function(event: Event) {
        if (event.type == PullParser.START_ELEMENT) {
            processStartEvent(event);
        } else if (event.type == PullParser.END_ELEMENT) {
            processEndEvent(event);
        }
    }

    protected var result = Result {};
    protected abstract function processStartEvent(event: Event): Void;
    protected abstract function processEndEvent(event: Event): Void;
}
...

ForecastResponseParser クラスは ResponseParser クラスのサブクラスです。更に ResponseParser クラスは javafx.data.pull.PullParser のサブクラスで、PullParser は XMLJSON データを解析するのに対応しています。ライブドアのお天気WebサービスXML で結果が返されるので、ここでは XML を解析します。onEvent メソッドは XML ドキュメントを構成する全ての要素について、開始タグと終了タグが内部で読み取られる毎に呼び出され、どちらが読み取られて呼び出されたのかは event 引数の type で判定できます。そしてここでは開始タグだったら processStartEvent メソッドを呼び出し、終了タグだったら processEndEvent を呼び出します。これらが abstract で宣言されているのは、天気予報とは別に地域情報を解析する為に、更に別のサブクラスも作成する必要があったからです。


XMLPullParser.fx

...
class ForecastResponseParser extends ResponseParser {

    override function processStartEvent(event: Event) {
        if (event.qname.name == "lwws" and event.level == 0) {
            result.forecast = Forecast {};
        } else if (event.qname.name == "location" and event.level == 1) {
            result.forecast.location = Location {};
            result.forecast.location.area = event.getAttributeValue(QName{name:"area"});
            result.forecast.location.pref = event.getAttributeValue(QName{name:"pref"});
            result.forecast.location.city = event.getAttributeValue(QName{name:"city"});
        } else if (event.qname.name == "image" and event.level == 1) {
            result.forecast.imageSource = ImageSource {};
        } else if (event.qname.name == "temperature" and event.level == 1) {
            result.forecast.temperature = Temperature {}
        }
        ...
    }

    override function processEndEvent(event: Event) {
        if (event.qname.name == "title" and event.level == 1) {
            result.forecast.title = event.text;
        } else if (event.qname.name == "link" and event.level == 1) {
            result.forecast.link = event.text;
        } else if (event.qname.name == "telop" and event.level == 1) {
            result.forecast.telop = event.text;
        } else if (event.qname.name == "description" and event.level == 1) {
            result.forecast.description = event.text;
        } else if (event.qname.name == "url" and event.level == 2) {
            result.forecast.imageSource.url = event.text;
        } else if (event.qname.name == "width" and event.level == 2) {
            result.forecast.imageSource.width = java.lang.Integer.parseInt(event.text);
        } else if (event.qname.name == "height" and event.level == 2) {
            result.forecast.imageSource.height = java.lang.Integer.parseInt(event.text);
        }
        ...
    }
}
...

ForecastResponseParser クラスは processStartEvent メソッドと processEndEvent メソッドをオーバーライドしています。これらのメソッドの中では if/else if で event.qname.name と event.level の値を調べて、Model であるデータオブジェクトを生成したり、値を設定したりしています。event.qname.name は XML の要素名を表していて、event.level は階層の深さを表しています。つまりこれらのメソッドは XML ストリームが読まれていく中で繰り返し呼び出されるので、欲しい情報が来たらデータを格納していく、という流れになります。これは気付く人もいると思いますが、SAX で XML を解析するのに似ていると思います。ここで注意点というかはまり所が一点。それは要素の属性を取得するには event.type が PullParser.START_ELEMENT のときに取得しないと駄目ということです。テキストデータは END_ELEMENT のイベントで取得すれば良いのですが、属性も含めて全て END_ELEMENT のイベントで取ろうとすると、思わぬ値が取れたりします。(カレント要素が子要素群を含んでいて、その中の子要素が同名の属性を持っていた場合に、最後の子要素の属性値が取得されてしまう)これは JavaFX のバグではないかとも思いましたが何とも言えません。とにかくこれでちょっとばかりはまってしまいました。


今日のところは以上です。
近いうち、お天気ウィジェットで使っている JFXtras というライブラリが、便利でおもしろいのでまたこのブログで触れてみたいなと思っています。


※JFXtras は WidgetFX の開発者の Stephen Chin さんらが開発した JavaFX 用の多目的なライブラリ
http://steveonjava.com/
http://code.google.com/p/jfxtras/