レベル: 中級 Federico Kereki, Systems Engineer, Freelance
2009年 7月 14日 近年広まっている Web 2.0 サイトでは、さまざまなソースからの情報をマッシュアップすることができます。GWT (Google Web Toolkit) を使用すると、RSS や、もっと新しい Atom 配信フォーマットなどの XML ベースのニュース・フィードを取得して処理することができます。この記事では、SOP (Same-Origin Policy: 同一生成元ポリシー) の制約を克服して任意の適切なフィードを利用できるようにし、フィードとして受信される XML データを処理する方法を学びます。
 |
よく使われる頭字語
- Ajax: Asynchronous JavaScript + XML
- API: Application Program Interface
- RSS: Really Simple Syndication
- URL: Uniform Resource Locator
- XML: Extensible Markup Language
|
|
近年広まっている Web 2.0 アプリケーションでは、アプリケーションの中で RSS フィードや Atom フィードのデータを取得して使いたい場合があるかもしれません。それを実現する方法としては、GWT と Ajax (Asynchronous JavaScript + XML) を利用することで、RSS フィードや Atom フィードをクライアント・サイドのコードで取得して処理する何通りかの方法があります。ただし、ブラウザーによる SOP などの制約を克服するために、いずれも何らかの対策が必要です。この記事では、フィードの取得と処理の両方に関する問題を解決した、簡単な GWT アプリケーションを検証します。この記事ではフル機能のフィード・リーダーを作成するわけではありませんが、皆さんの Web アプリケーションに RSS とAtom によるフィードを利用できるようになるための指針を提供します。
フィードを取得する
まず、フィードを取得する方法を考えましょう。通常は Ajax を使用します。しかし SOP の問題 (「SOP の問題」を参照) があるため、GWT ページをフィード・ソースに接続することはできません。もちろん、皆さんのサーバー自体がフィード・ソースであれば別ですが、それではあまり面白くありません。
SOP の問題
同一生成元ポリシーはブラウザーのセキュリティーによる制約です。この制約により、ある 1 つの生成元 (1 つの生成元という言葉が意味するのは、最初に要求した URL と同じプロトコル、同じホスト、同じポートを持つ URL のことです) からロードされたページのスクリプトなどが、異なる生成元のデータにアクセスすることはできません (Windows® Internet Explorer® は SOP の制約が緩く、ポートが変更されていても無視しますが、それは標準の動作ではありません)。例えば、ある Web ページが http://www.yourownsite.com:80/some/page からロードされた場合、SOP があるため、そのページのスクリプトなどが以下を始めとする他の URL からデータを取得することはできません。
- https://www.yourownsite (プロトコルが変更されている)
- http://othersite (ホストが変更されている)
- http://www.yourownsite.com:8080 (ポートが変更されている)
セキュリティー上の目的としては、SOP は確かにとても理に適っています。SOP があることによって、1 つの生成元から得られたページのスクリプトなどが、異なる生成元のデータにアクセスして、そのデータを操作、表示することはできなくなるからです。SOP がなくなれば、フィッシング詐欺を行う者達にとっては理想的な環境になります。信頼できる Web サイトの有効なページを見ている場合でも、その際のアクションや送受信されるデータを第三者がモニターできてしまうからです。SOP があるおかげで、どのようなデータを受信する場合も、そのデータが確かに最初に要求した Web サイトから送信されたものであることが保証されます。他の (おそらく怪しい) 生成元からのコードは、どのようなコードであっても受信することはできないのです。
この制約があることを考慮すると、フィードを取得する方法として考えられる選択肢は以下の 2 つです。
- Ajax を使って皆さんのサーバーのプロキシーに接続します。そのプロキシーがフィードを取得し (コードがブラウザーで実行されているのでなければ SOP の制約はありません)、そのフィードをページに返送する方法。
- JavaScript に関連する (Google の API を利用した) 興味深い手法を利用して SOP をバイパスする方法。
この記事では最初にプロキシーを使用する手法を学び、次に Google AJAX Feed API を使用する方法を学びながら、Java™ と JavaScript のコーディングを混在させる方法を学びます。
Web ページの設計
まず、基本となるページを設計します。このページには、必要なフィードの URL を入力するためのテキスト・ボックス、フィールドの取得方法を選択するためのリスト・ボックス (実際には、こうした選択肢をユーザーに与えることはありません)、そしてデータの取得を実行するためのコマンド・ボタンがあります。図 1 はまだフィードを取得していない状態の基本となるウィンドウを示しており、デフォルトの URL が表示されています。ところで、この図で使用しているブラウザーがどのブラウザーなのかわからない人のために言うと、これは GWT 独自の、ホスト・モードの Mozilla ベースのブラウザーです。もちろん、このアプリケーションをコンパイルしてデプロイした後は、任意のブラウザーを使うことができます。
図 1. まだフィードを取得していない状態のページ
フィードを取得すると、何も特別な処理はせず、ニュースのメイン・タイトル、簡単な説明、そしてリンクを単純に表示します。このページでは取得されるフィードの数に制限はなく、新しいデータが表示される前には、それまで表示されていたデータは画面から消去されます。図 2 は実際にフィードの取得を実行した例を示しています。
図 2. フィードを取得した結果
まとめとして、このプログラムの全体的な構造を見てください (リスト 1)。わかりやすくするために、ここに示したコードは各メソッドの簡単な説明を付けてかなり省略してあります。後ほど各メソッドのコードを検証します。オリジナルの完全なソース・コードは「ダウンロード」セクションを参照してください。
リスト 1. 主なコードの全体的な構造
package com.fkereki.rssread.client;
//... "import" lines ...
public class Rssreader implements EntryPoint {
// ... variable definitions...
public void onModuleLoad() {
// set up the form and its fields
// call getFeedViaProxy(...) or getFeedViaGoogle(...)
// depending on the listbox value
}
void getFeedViaProxy(final String feedUrl) {
// connect to the remote server via RPC
// when data arrives, call processAndShowFeed(...)
}
native void getFeedViaGoogle(final String feedUrl) /*-{
// call Google Feed API (using native JavaScript, not Java)
// when data arrives, call processAndShowFeed(...)
}-*/;
void processAndShowFeed(final String xmlDocument) {
// clear results from a previous run, if any
// decide whether it's RSS or Atom, and call
// processRssFeed() or processAtomFeed() as required
}
void processRssFeed(final Element root) {
// navigate a RSS feed, extraction titles, descriptions,
// and links, and using showFeedItem(...) to show them
}
void processAtomFeed(final Element root) {
// navigate an Atom feed, extraction titles, descriptions,
// and links (by using getValueIfPresent(...) and
// getLinkIfPresent(...), and showFeedItem(...) to show them
}
private String getValueIfPresent(final Element el, final String tn) {
// get an XML node, and return the value that corresponds to a certain tag
}
private String getLinkIfPresent(final Element el) {
// given a XML "link" node, return the corresponding address
}
private void showFeedItem(final String title, final String description,
final String link) {
// add some lines to the screen, with the data for the latest news
}
}
|
プロキシーを使う
GWT では、RPC (Remote Procedure Call) を使うことでサーバー・サイドのサーブレットに容易にアクセスすることができます。しかしそのためには、クライアント・サイド・コード用のいくつかのインターフェースと、サーバー・サイド・コードとしての実際のサーブレットのプログラムを作成する必要があります。最初にサーバー・サイドのコードを考えましょう。この場合に必要なサービスは、フィードの URL が指定されたらそのサイトに接続し、サイトのコンテンツをダウンロードして、そのコンテンツを呼び出し側に返送することです。(対応するシェル・コマンドとして wget コマンドや curl コマンドを考えてみてください)。そのための方法はたくさんありますが、簡単に実現する方法をリスト 2 に示します。ここではリモート・プロキシーを RemoteProxy と呼ぶことに決めたので、サーバー・サイドのクラスを RemoteProxyImpl と呼ぶようにします (Impl は「Implementation (実装)」を表します)。
リスト 2. GWT のサーブレット・プロキシー
package com.fkereki.rssread.server;
//... "import" lines...
public class RemoteProxyImpl
extends RemoteServiceServlet implements RemoteProxy {
//... variable definitions ...
public String getFeed(final String feedUrl) {
String result= "";
try {
final BufferedReader in= new BufferedReader(new InputStreamReader(
new URL(feedUrl).openStream()));
String inputLine;
while ((inputLine= in.readLine()) != null) {
result+= inputLine;
}
in.close();
return result;
} catch (final Exception e) {
return "";
}
}
}
|
サーバー・サイドのコードが作成できたので、今度はクライアント・サイドに必要なコードの作成に移りましょう。GWT の命名規則に従って、リスト 3 の 2 つのインターフェース (RemoteProxy と RemoteProxyAsync) を実装する必要があります。この 2 つのインターフェースを使って Ajax スタイルで非同期コールバックを行うと、リモート・プロキシーが値を返します。
リスト 3. RPC による呼び出しのために 2 つのインターフェースが必要なクライアント・サイド
@RemoteServiceRelativePath("remoteProxy")
public interface RemoteProxy extends RemoteService {
public String getFeed(String feedUrl);
}
//
public interface RemoteProxyAsync {
void getFeed(java.lang.String feedUrl,
com.google.gwt.user.client.rpc.AsyncCallback<String> arg2);
}
|
この 3 つのコードを組み合わせると、リスト 4 のように容易にフィードを取得することができます。
リスト 4. 上記のインターフェースを使って Ajax スタイルのコールバックを行う
void getFeedViaProxy(final String feedUrl) {
final RemoteProxyAsync rp= (RemoteProxyAsync)GWT
.create(RemoteProxy.class);
rp.getFeed(feedUrl, new AsyncCallback<String>() {
public void onFailure(final Throwable caught) {
Window.alert("failure?!");
}
public void onSuccess(final String result) {
processAndShowFeed(result);
}
});
}
|
フィードの URL が指定されたら、RPC の詳細部分を処理する適当なクラスを GWT のメカニズムを使って作成します (詳細部分というのは、データのシリアライズ、コールバックの設定、サーバー・サイドのサーブレットの実際の呼び出し、受信された応答のデシリアライズ、などです)。また、非同期コールバックのプログラムも作成し、データが返送された場合に何をするのかを以下のように指定します。
- エラーが発生すると、
onFailure メソッドが呼び出されます。
- RPC による呼び出しに成功すると
onSuccess メソッドが呼び出され、この場合は受信された XML を処理してフィードを画面上に表示します。
Google AJAX Feed API を使う
通常、ブラウザーには SOP の制約があるため、別のサイトからのデータをコードで取得することはできませんが、例外があります。その例外とは、<script ... /> タグを使うと JavaScript コードをダウンロードして実行することができるのです。もしダウンロードしたコードがデータを含んでおり、しかもそのデータを適切に利用するためにユーザーが用意した関数を呼び出すとすると、SOP をバイパスできたことになります。Google AJAX Feed API の背後にある考え方は以下のとおりです。
<script ... /> タグを使って、プロキシーとして動作する Google のサイトを呼び出します。
- リモート・サイトがフィード・データを取得し、そのデータを JavaScript コードの形式で返します。
- ダウンロードされた JavaScript コードが、ユーザーが用意した関数を呼び出し、受信される XML をその関数が処理します。
Google AJAX Feed API は JavaScript コードで作成されているため、GWT の JSNI (JavaScript Native Interface) を使う必要があります。まず、リスト 5 のように jsapi スクリプトを含めます。Google のサイトから直接 jsapi スクリプトを含めることも (このことから、この状況には SOP が適用されないことが確認できます)、皆さんのサーバーから jsapi スクリプトをダウンロードして含めることもできます。このスクリプトによって、後ほど使用するグローバルな google オブジェクトが作成されます。リスト 5 の “mandatory initialization” (強制的な初期化) の行からわかるように、このグローバルな google オブジェクトは、使用する前に、初期化する必要があります。
リスト 5. Google AJAX Feed API 関連のコーディングを含む Web ページ
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<!-- ... -->
</head>
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("feeds", "1"); // mandatory initialization
</script>
<body>
<!-- ... -->
</body>
</html>
|
リスト 6 は必要な JSNI コードを示しています。ここではすべての詳細は説明しませんが、以下のことに注意してください。
native アクセス修飾子によって、getFeedViaGoogle メソッドが Java コードではなく JavaScript コードでプログラミングされていることを強調しています。
/*-{ と }-*/ はメソッドの実装の区切りです。
- グローバル変数
$wnd は google 変数にアクセスします (GWT はコンパイルされたアプリケーション・コードを子の iframe にロードします。そのため、メイン・ウィンドウの中のどれに直接アクセスしようとしても失敗します)。
- 受信された XML 文書に対して
serializeToString パラメーターがあるのは、processAndShowFeed メソッドが String パラメーターを要求するためです。
- 少し複雑な構文が必要な理由は、Java メソッドを呼び出すためです。
- (
this を直接使う代わりに) myself 変数を使っている理由は、こうしないとクロージャーが機能しないからです (「参考文献」を参照)。
リスト 6. Google AJAX Feed API と JSNI を使ってフィードを取得する
native void getFeedViaGoogle(final String feedUrl) /*-{
var myself= this;
var feed= new $wnd.google.feeds.Feed(feedUrl);
feed.setResultFormat($wnd.google.feeds.Feed.XML_FORMAT);
feed.load(function(xmlResult) {
myself.@com.fkereki.rssread.client.Rssreader::processAndShowFeed(Ljava/lang/String;)
((new XMLSerializer()).serializeToString(xmlResult.xmlDocument));
});
}-*/;
|
JSNI の詳細を別にすると、このコードは単純です。取得対象の URL を指定する feed オブジェクトを作成し、取得するフォーマットを XML に設定します。そして load 関数を呼び出してコールバック関数を引数として渡し、このコールバック関数によってフィードを (ストリングに変換して) processAndShowFeed メソッドに渡しています。
フィードを使用する
RSS フィードも Atom フィードも XML 文書であるため、GWT の XMLParser をそのまま使用することができます。空ではない XML ストリングがある場合、この XMLParser を使用して XML ストリングを構文解析し、ルート要素を取得します。そのフィードが RSS なのか Atom なのかを容易に判断するためには、ルート・ノードの名前を調べます。RSS であれば名前が rss であり、Atom であれば名前が feed です。一致するものがない場合には、エラーを生成します。最後に、適切なメソッドを呼び出し、その XML 文書をウォークスルーして、表示用の部分を取得します (リスト 7)。
リスト 7. フィードを表示する
void processAndShowFeed(final String xmlDocument) {
if (xmlDocument.isEmpty()) {
// warn about the problem; most likely, a wrong URL
} else {
final Document xmlDoc= XMLParser.parse(xmlDocument);
XMLParser.removeWhitespace(xmlDoc);
final Element root= xmlDoc.getDocumentElement();
// clear out the previous feed results, if any,
// to make space for the feed that is to be loaded
if (root.getNodeName().equals("rss")) {
processRssFeed(root);
} else if (root.getNodeName().equals("feed")) {
processAtomFeed(root);
} else {
// warn about the unknown feed format
}
}
}
|
リスト 8 は (少し省略した) RSS フィードを示しています。
リスト 8. (少し省略した) RSS ニュース・フィード
<?xml version="1.0" encoding="ISO-8859-1"?>
<rss xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
<channel>
<title>CNN.com - Technology</title>
<link>http://edition.cnn.com/TECH/?eref=edition_technology</link>
<description>CNN.com delivers up-to-the-minute news ...</description>
<language>en-us</language>
<copyright>© 2009 Cable News Network LP, LLLP.</copyright>
<pubDate>Thu, 16 Apr 2009 20:56:04 EDT</pubDate>
<ttl>10</ttl>
... several more channel properties
<item>
<title>YouTube orchestra wows Carnegie Hall</title>
<link>http://rss.cnn.com/~r/rss/edition_technology/~3/XxF062aMfCI/index.html</link>
<description>The YouTube and Carnegie Hall generations ...</description>
<pubDate>Thu, 16 Apr 2009 15:28:47 EDT</pubDate>
</item>
<item>
... another news item...
</item>
<item>
... yet another item...
</item>
</channel>
</rss>
|
RSS の XML フィードを処理するためのメソッドと Atom の XML フィードを処理するためのメソッドは、ほとんど同じです。基本的に、すべてのニュース・ノード (RSS の場合は item、Atom の場合は entry) を取得するには getElementsByTagName を使います。注意する点として、RSS の場合には 1 つレベルを下げ、中間の channel ノードをスキップする必要があります (リスト 9)。次に、ニュース項目を 1 つずつ調べ、title、description (Atom の場合は summary)、そして link を抽出して表示します。getValueIfPresent メソッドは title と description を処理します。getLinkIfPresent メソッドは RSS と Atom との間の違いを処理します (RSS の場合はノードの値は実際のリンクですが、Atom の場合には href 属性を使う必要があります)。
リスト 9. RSS フィードを処理するためのコードの一部
void processRssFeed(final Element root) {
final NodeList items=
((Element)root.getElementsByTagName("channel").item(0)).
getElementsByTagName("item");
for (int i= 0; i < items.getLength(); i++) {
final Element item= (Element)items.item(i);
final String rssDescription= getValueIfPresent(item, "description");
final String rssTitle= getValueIfPresent(item, "title");
final String rssLink= getLinkIfPresent(item);
// display rssTitle, rssDescription and rssLink onscreen
}
}
private String getValueIfPresent(final Element el, final String tn) {
final NodeList nl= el.getElementsByTagName(tn);
if (nl.getLength() == 0) {
return "";
} else {
return nl.item(0).getFirstChild().getNodeValue();
}
}
private String getLinkIfPresent(final Element el) {
final NodeList nl= el.getElementsByTagName("link");
if (nl.getLength() == 0) {
return "";
} else {
if (nl.item(0).hasChildNodes()) {
return nl.item(0).getFirstChild().getNodeValue();
} else {
return ((Element)nl.item(0)).getAttribute("href");
}
}
}
|
ここで、白状しておかなければならないことと、注意しなければならないことが 1 つあります。この記事に付属の Atom フィードを処理するコードは、あまり深く考えず、summary が表示可能なテキストだと想定しています。より安全なコードにするためには、type 属性を調べ、表示可能であることを確認する必要があります。場合によると summary は表示不可能なものかもしれません (例えば XML、base64 でエンコードされたバイナリー・コンテンツ、あるいは他のコンテンツへのポインターなど)。しかしその可能性は低いため、ここではコードを単純にしました。従って、本当に一般的な Atom リーダーを作成したい場合には、以上の点に注意してください。
まとめ
この記事では、GWT の Web ページから RSS フィードと Atom フィードを直接取得して処理するための 2 つの異なる方法を説明しました。ここで紹介したコードは、XML フィードそのものを直接扱うことで、基本となるフィード処理を行います。このコードを拡張し、より高度な処理をすることも、それほど複雑ではないはずです。しかもどんなフィードでも皆さんの Web ページで使用できるのです。ぜひ試してみてください。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| Sample GWT code for this article | rssreader.tar.gz | 4305KB | HTTP |
|---|
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | Federico Kereki はウルグアイ人のシステム・エンジニアであり、システム開発やコンサルティング、大学での教育に 20 年を超える経験があります。現在はおなじみの頭字語、SOA、GWT、Ajax、PHP、そしてもちろん FLOSS を渾然一体に扱いながら業務を行っています。 |
記事の評価
|