JavaでRSSの広告削除してみる(3) Removing Ads from Rss by Java

RSSから広告削除する、簡単なアプリをつくってみようかと思っています。
3回に分けて書いています。I write this in 3-posts.

第1回 JavaでRSS受信する。 Getting RSS by Java
第2回 HTMLパーサを使う。 Using HTML-parser
第3回 RSSを出力してみる。 Outputting RSS

今回は第3回めです。This is 3rd post.

————————————————————–
使っている環境

Java JDK1.6.0_35(64bit版)
Google App Engine SDK for Java 1.7.1
Eclipse IDE for Java EE Developers_ Juno (4.2)64Bit版
The Google Plugin for Eclipse, for Eclipse 3.8/4.2 (Juno)

ROME 1.0
JDOM 1.1.3
( Xerces 2.11.0 )
jsoup-1.7.2


(1)はじめに Introduction
クラスを作ってその中で「RSS読込→広告削除→RSS出力」の処理を行いたいと思います。
RSS読込・広告削除は、前回・前々回のエントリーで行ったので、今回は、まずRSS出力を試してみます。

In my Class, I would try to do entire process of “RSS-reading”, “removing-Ads”, and “RSS-outputting”.
I did “RSS-reading” and “removing-Ads” in previously post. Today, I will try to do “RSS-outputting” first.


(2)RSSの出力 Outputting RSS
SyndFeedOutput クラスのoutputString メソッドで、XMLを出力できます。
ROMEでRSSを読込んだ時のfeedを、outputString メソッドに引き渡す感じになります。
You could output XML by using outputString method of SyndFeedOutput class.
You execute outputString method with the “feed” that made by ROME .
(参考) 技術への名残り: JavaでRSSを作成(ROME利用)

        SyndFeedOutput output = new SyndFeedOutput();
        String outputRss = output.outputString(feed);

(3)コーディング Coding
CrawlRssクラスを作って、その中で処理を行うことにしました。
処理の流れはこんな感じ。
I thought that I would make “CrawlRss class” and do what’s I need.
I will show you my plan(below)

・setUrlメソッド
接続先URLをセットします。Setting URL

・getRssFeedメソッド
RSSを読込みます。このメソッドは、取得したデータ(SyndFeed型)を返します。
Reading RSS. this method will return that data as SyndFeed-type.

・doFilterメソッド
RSSのタイトルが、広告かどうかをチェックして、広告の場合は削除します。
RSSのdescriptionをチェックして、中に含まれる広告を削除します。
(広告かどうか判別する処理は、今後も増えていくため、別メソッドに書いています。
タイトルのチェックはtitleIsAdsメソッド、descriptionのチェックはhtmlParsingメソッドの中で、具体的なチェックを行っています。)

Checking “Title” of RSS-entry. and If the Title is Ads, it’s remove the RSS-entry.
Checking “description” of RSS-entry. And it’s remove Ads in the description.

・outputRssメソッド
RSSfeedデータから、RSSを出力します。String型でXMLデータを返します。
This output RSS generated from RSSfeed-data. This return RSS as XML-data.

CrawlRss.java

package rssReader;

import java.io.IOException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.SyndFeedOutput;
import com.sun.syndication.io.XmlReader;
import com.sun.syndication.feed.synd.SyndContent; 

public class CrawlRss {
    private String url;

    /** 
     * XXX 
     *
     * @param  url  URL for getting RssFeed
     * @param  feed  RssFeed generated by ROME
     */
    public void setUrl(String url) {
        this.url = url;
    }

    public String getUrl() {
        return this.url;
    }

    public SyndFeed getRssFeed() throws IOException  {
        SyndFeed feed = null;

        if ( this.url == null ){
            System.out.println("Parameter( url ) didn't set.");
            return null;
        }else{
            try {
                URL feedUrl = new URL(this.url);
                XmlReader reader = new XmlReader(feedUrl.openStream());
                    SyndFeedInput input = new SyndFeedInput();
                    feed = input.build(reader);
                reader.close();

            } catch (FeedException e) {
                e.printStackTrace();
            }
        }

        return feed;
    }

    public SyndFeed doFilter(SyndFeed feed){

        List<SyndEntry> entries = feed.getEntries();
        Iterator<SyndEntry> itr = entries.iterator();

        while (itr.hasNext()){
            SyndEntry entry = itr.next();

            // Removing Ads from Title
            String title = entry.getTitle();
            if ( titleIsAds(title)) {
                itr.remove();
                continue;
            }

            // Removing Ads from Desc
            SyndContent desc = entry.getDescription();

            if (desc != null){
                String html_parsed = htmlParsing(desc.getValue());
                desc.setValue(html_parsed);
            }
        }

        return feed;
    }

    public boolean titleIsAds(String title){
        if ( title.startsWith("PR:")) {
            return true;
        }else{
            return false;
        }

    }

    public String htmlParsing(String html){

        Document doc = Jsoup.parse(html, "UTF-8");
        Elements links = doc.getElementsByTag("a");

        for (Element link : links) {
            String href = link.attr("href");

            if ( href.indexOf("rss.rssad.jp/rss/ad/") != -1 ) {
                link.remove();
            }
        }

        Elements imgs = doc.getElementsByTag("img");

        for (Element img : imgs) {
            String width = img.attr("width");
            String height = img.attr("height");

            if ( width.equals("1") && height.equals("1") ) {
                img.remove();
            }
        }

        String bodies_str = doc.getElementsByTag("body").toString();
        bodies_str = bodies_str.replaceFirst("^<body>\n", "");
        bodies_str = bodies_str.replaceFirst("\n</body>$", "");
        bodies_str = bodies_str.replaceFirst("^ +", "");

        return bodies_str;
    }

    public String outputRss(SyndFeed feed) throws FeedException{
        SyndFeedOutput output = new SyndFeedOutput();
        String outputRss = output.outputString(feed);

    return outputRss;
    }
}

実際に動かす場合は、こうします。
Here is my code to use the Class.

package rssReader.test;

import java.io.IOException;

import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;

import rssReader.CrawlRss;

public class unitTest_output_XML {
    public static void main(String[] args) throws IOException, FeedException {

        CrawlRss crss = new CrawlRss();

        crss.setUrl("http://rss.rssad.jp/rss/gihyo/feed/rss2");
        // crss.setUrl("http://jp.techcrunch.com/feed/");
        // crss.setUrl("http://wired.jp/rssfeeder/");

        //System.out.println("Access to : " + crss.getUrl() + "\n\n");

        SyndFeed feed = crss.getRssFeed();

        if (feed == null){
            System.out.println("Exit");
            System.exit(-1);
        }

        feed = crss.doFilter(feed);

        String outputRss = crss.outputRss(feed);
        System.out.println(outputRss);

    }

}

こんな感じでXMLが返ってきます
That code will return these XML(below).

<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
  <channel>
    <title>gihyo.jp:総合</title>
    <link>http://gihyo.jp/</link>
    <description>gihyo.jp(総合)の更新情報をお届けします</description>
    <language>ja-jp</language>
    <copyright>技術評論社 2013</copyright>
   :   :

    <item>
      <title>第6回 Capacity Scheduler による複数ジョブの同時実行 ── halookで始めるHadoop/HBaseトラブルシューティング</title>
      <link>http://rss.rssad.jp/rss/artclk/47ghQF72z411/6d70ced8b37632c22bd6add0ae4e4982?ul=tMUp6phKmaEl5PP2lXYGvZcEI0UboxMPKoYv1ONSRKgTtDvugPJtMP3M.QKxrTI5bkjb3.g</link>
      <description>今回は,Capacity Schedulerを使った場合について,複数ジョブの同時実行時の挙動を見てみましょう。</description>
   :   :

    </item>
    <item>
   :   :

今のところ、TechCrunch Japan、WIRED.jp、gihyo.jp のRSSfeedから広告削除できるようになっています。もうちょっと、チェックの条件を増やして、たいていのRSSに対応できるような形にできればいいなと、思っています。
This code supports removing Ads from RSS of “TechCrunch Japan”, “WIRED.jp” “gihyo.jp”.
I hope that I would add more rule to the code for removing Ads, so it could support more RSS.

それをGoogleAppEngine上で動かすようにすれば、APIとして公開できるかな。(同じようなサービスは、たくさんあるけど(笑))
And then,  I would deploy the code on GAE as an API. (There is many Service for Romoving Ads  from RSS 🙂  )

JavaでRSSの広告削除してみる(2) Removing Ads from Rss by Java

RSSから広告削除する、簡単なアプリをつくってみようかと思っています。
3回に分けて書いています。I write this in 3-posts.

第1回 JavaでRSS受信する。 Getting RSS by Java
第2回 HTMLパーサを使う。 Using HTML-parser
第3回 RSSを出力してみる。 Outputting RSS

今回は第2回めです。This is 2nd post.

————————————————————–
使っている環境

Java JDK1.6.0_35(64bit版)
Google App Engine SDK for Java 1.7.1
Eclipse IDE for Java EE Developers_ Juno (4.2)64Bit版
The Google Plugin for Eclipse, for Eclipse 3.8/4.2 (Juno)

jsoup-1.7.2


(1)はじめに Introduction
実際にRSS取得してみると、記事丸ごとが広告になっている場合と、要約の中に広告が埋まっている場合があります。
たとえば、タイトルが”PR:”で始まっているものは、記事全体が広告になってる。これはタイトルの文字列をチェックするだけなので取り除くのは簡単です。
I get RSS, then I found that some article are Ads, and some description contain Ads.
For example, if “Title” start with “PR:”, this articl is Ads. Checking Title is easy.

PR: 医療機関ホームページの虚偽・誇大な表現はNGです!

要約の中に広告が埋まっている場合は、以下のような形で、広告のリンクが埋まっています。
また、サイズが1×1の画像ファイルが埋め込まれていたりします。これも要らない。
This is example that a description contain “link to Ads”.  And there is “img” that it has 1x1size. this “img” have to be removed too.

<p>前回紹介したように,gmtpに文字コード回りのパッチをあてることで,無事Plamo Linux環境とNexus 7の間でファイルをやりとりできるようになりました。しかしながら,あれこれ使っているとgmtpの欠点も目についてきました。
<img border="0" width="1" height="1" src="http://rss.rssad.jp/rss/artimg/47ghQF72z411/3184e1aa6c761b751f68c2613f8a3f55"/></p>
<br clear="all" /><a href="http://rss.rssad.jp/rss/ad/47ghQF72z411/pFjyc8QFo.Eh?type=1" target="_blank"><img src="http://rss.rssad.jp/rss/img/47ghQF72z411/pFjyc8QFo.Eh?type=1" border="0"/></a><br/>

これらを取り除く場合、HTMLをParseする必要があるため、HTMLパーサのライブラリを使い、不要な部分のみを取り除くことにします。
You should use HTMP-parsing-library to remove these words.


(2)jsoupのダウンロード Downloading jsoup
そもそもHTMLのパーサは、たくさんあります。There are many library.
Comparison of HTML parsers – Wikipedia, the free encyclopedia

活発に開発されていてHTML5にも対応しているものがいいと思いました。
jsoupはHTML5にも対応しており、jQuery-likeなやり方でも使えるとのことで、なかなかよさそうです。
なお、Unitテストといったことを行いたい場合は、HtmlUnitがいいみたいです。
I should use the library that is developed more active , and support  HTML5.
“jsoup” support HTML5, and jQuery-like method. I think it’s ok.
If you wish to do Unit-test , HtmlUnit would be suitable.
What are the pros and cons of the leading Java HTML parsers? – Stack Overflow
HtmlUnit vs JSoup: HTML Parsing in Java | Javalobby

今回はHTMLのparsingが目的なので、jsoupを使うことにしました。
こちらのサイトから、jsoup-1.7.2.jar をダウンロードし、Eclipseのライブラリに追加します。
I wish to use library for parsing HTML. So I chose jsoup.
you can download jsoup-1.7.2.jar in this site. And you should add it to Eclipse-library .
jsoup Java HTML Parser, with best of DOM, CSS, and jquery


(3)とりあえず読込んでみる Reading simply
テストデータとして以下の内容を、テキストファイル(testdata_html.txt)に書き込んでおき、jsoupを動かしてみました。
I tried to use jsoup. I made test-data in a text file(testdata_html.txt). I show you the test-data below.

<p>前回紹介したように,gmtpに文字コード回りのパッチをあてることで,無事Plamo Linux環境とNexus 7の間でファイルをやりとりできるようになりました。しかしながら,あれこれ使っているとgmtpの欠点も目についてきました。
<img border="0" width="1" height="1" src="http://rss.rssad.jp/rss/artimg/47ghQF72z411/3184e1aa6c761b751f68c2613f8a3f55"/></p>
<br clear="all" /><a href="http://rss.rssad.jp/rss/ad/47ghQF72z411/pFjyc8QFo.Eh?type=1" target="_blank"><img src="http://rss.rssad.jp/rss/img/47ghQF72z411/pFjyc8QFo.Eh?type=1" border="0"/></a><br/>

基本的に公式ドキュメントがとてもわかりやすかったので、それを参考にしました。
I check this documents for use .
Cookbook: jsoup Java HTML parser

jsoupでテストデータを読込むと、不足している要素・タグが追加され、HTMLファイルの構成で、内部に格納されます。<html>タグや、<head>タグが追加されます。
When jsoup get test-data, jsoup make entire HTML elements. If test-data didn’t have “<html>-tag” or “<head>-tag”, jsoup would add these tags.

parsingした結果をブラウザで確認してみようと思ったので、<head>タグ内に文字コード設定を追加することにしました。
で、とりあえずテストデータを読込んで、それをそのままjsoupで出力させるコードを以下に書きます。
I think I practised jsoup and I would check the result by Browser. So I added char-setting in “<head>-tag”.
If you execute this code, it’s show you the result of “Reading RSS” and “outputting RSS-parsing-data”.

HtmlParsing.java

import java.io.File;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

public class HtmlParsing {
    public static void main(String[] args) throws Exception {

        File input = new File("testdata_html.txt");
        Document doc = Jsoup.parse(input, "UTF-8");

        Element head = doc.select("head").first();
        head.html("<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">");

        System.out.println("--- doc -----");
        System.out.println(doc + "\n");

        System.out.println("--- doc text only-----");
        System.out.println(doc.text() + "\n");

    }
}

実行結果は、こんな感じになります。
You can get this when you execute that code(above).

--- doc -----
<html>
 <head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
 </head>
 <body>
  <p>前回紹介したように,gmtpに文字コード回りのパッチをあてることで,無事Plamo Linux環境とNexus 7の間でファイルをやりとりできるようになりました。しかしながら,あれこれ使っているとgmtpの欠点も目についてきました。 <img border="0" width="1" height="1" src="http://rss.rssad.jp/rss/artimg/47ghQF72z411/3184e1aa6c761b751f68c2613f8a3f55" /></p> 
  <br clear="all" />
  <a href="http://rss.rssad.jp/rss/ad/47ghQF72z411/pFjyc8QFo.Eh?type=1" target="_blank"><img src="http://rss.rssad.jp/rss/img/47ghQF72z411/pFjyc8QFo.Eh?type=1" border="0" /></a>
  <br />
 </body>
</html>

--- doc text only-----
前回紹介したように,gmtpに文字コード回りのパッチをあてることで,無事Plamo Linux環境とNexus 7の間でファイルをやりとりできるようになりました。しかしながら,あれこれ使っているとgmtpの欠点も目についてきました。

(4)Parsing
広告の削除と、サイズが1×1の画像ファイルの削除を行います。
I will remove Ads and img-files that has 1x1size.

<a>タグの一覧を取得し、それぞれのAttribute”href”の値を取得します。その中に”rss.rssad.jp/rss/ad/”が含まれていたら、<a>タグごと削除します。これで広告の削除ができます。
This code gets <a>-tags and each “href” Attribute. If there are the words “rss.rssad.jp/rss/ad/”, it’s remove entire <a>-tag. Then you could remove ads.

また、<img>タグの一覧を取得し、それぞれのAttribute”width”、”height”の値を取得します。width=height=1の場合、<img>タグごと削除します。
And it’s gets <img>-tags, each “width” Attribute and each “height” Attribute. If those Attribute are width=height=1, you chould remove entire <img>-tag.

ソースコードは、こんな感じになりました。
this is my code.

HtmlParsing2.java

import java.io.File;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

public class HtmlParsing2 {
    public static void main(String[] args) throws Exception {

        File input = new File("testdata_html.txt");
        Document doc = Jsoup.parse(input, "UTF-8");

        Element head = doc.select("head").first();
        head.html("<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">");

        Elements links = doc.getElementsByTag("a");

        for (Element link : links) {
            String href = link.attr("href");

            // System.out.println("href : " + href);
            // System.out.println("link.text : " + link.text());
            // System.out.println("link : " + link);

            if ( href.indexOf("rss.rssad.jp/rss/ad/") != -1 ) {
                link.remove();
            }
        }

        Elements imgs = doc.getElementsByTag("img");

        for (Element img : imgs) {
            String width = img.attr("width");
            String height = img.attr("height");
            // System.out.println("img : " + img);

            if ( width.equals("1") && height.equals("1") ) {
                img.remove();
            }
        }

        System.out.println("--- doc2 -----");
        System.out.println(doc + "\n");
        System.out.println("--- doc text only-----");
        System.out.println(doc.text() + "\n");
        System.out.println("--- body-----");
        System.out.println(doc.getElementsByTag("body") + "\n");
    }
}

実行結果は、こんな感じ。
I will how you the result.

--- doc2 -----
<html>
 <head>
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
 </head>
 <body>
  <p>前回紹介したように,gmtpに文字コード回りのパッチをあてることで,無事Plamo Linux環境とNexus 7の間でファイルをやりとりできるようになりました。しかしながら,あれこれ使っているとgmtpの欠点も目についてきました。 </p> 
  <br clear="all" />
  <br />
 </body>
</html>

--- doc text only-----
前回紹介したように,gmtpに文字コード回りのパッチをあてることで,無事Plamo Linux環境とNexus 7の間でファイルをやりとりできるようになりました。しかしながら,あれこれ使っているとgmtpの欠点も目についてきました。

--- body-----
<body>
 <p>前回紹介したように,gmtpに文字コード回りのパッチをあてることで,無事Plamo Linux環境とNexus 7の間でファイルをやりとりできるようになりました。しかしながら,あれこれ使っているとgmtpの欠点も目についてきました。 </p> 
 <br clear="all" />
 <br />
</body>

これで、やりたいことはできました。
Then I did.

最終的にRSSのフィードに戻すときに、<body>, </body>タグが不要になります。なので、以下のようにして、Elements型を文字列にしてから取り除くことにします。(※不要な改行と、先頭の空白文字も取り除いています。)
When you return this result to RSS-feed, <body>tag and </body>-tag didin’t need.
So I transfered type-“elements” to String, and then removed these elements . ( I removed “\n” and “blanks at the head of line”)

     :   :
        String bodies_str = doc.getElementsByTag("body").toString();
        bodies_str = bodies_str.replaceFirst("^<body>\n", "");
        bodies_str = bodies_str.replaceFirst("\n</body>$", "");
        bodies_str = bodies_str.replaceFirst("^ +", "");
     :   :

とりあえず、これで下準備がおわりました。
次回のエントリーでは、「RSS読込→広告削除→RSS出力」までつなげてみます。

In next post, I would try to do entire process of “RSS-reading”, “removing-Ads”, and “RSS-outputting”.

JavaでRSSの広告削除してみる(1) Removing Ads from Rss by Java

IT関連の情報サイトから、RSSフィードの情報収集して、その中の単語を解析・集計して、自分用のトレンドランキングをつくろうかと思った。
I come up with making “trend word list” for myself. I try to collect RSS from IT-website, analyze RSS-feed, and rank it.

ためしにRSS受信してみると、RSSフィードの中に広告が入っている場合がある。広告は削除しておきたい。Some RSS contain advertisement. I hope to remove Ads.

RSSから広告削除してくれるサービスがるんだけれども、There are some service to remove Ads from RSS.
RSS広告を削除ならRSS広告削除社

gihyo.jpのRSSの広告がうまく削除されなかった。But, that service didn’t remove Ads from “gihyo.jp”.
というわけで、自分でつくってしまおうかと思った。So I think of trying to make app.

3回に分けて書いていきます。I write this in 3-posts.

第1回 JavaでRSS受信する。 Getting RSS by Java
第2回 HTMLパーサを使う。 Using HTML-parser
第3回 RSSを出力してみる。 Outputting RSS

今回は第1回めです。This is first post.

————————————————————–
使っている環境

Java JDK1.6.0_35(64bit版)
Google App Engine SDK for Java 1.7.1
Eclipse IDE for Java EE Developers_ Juno (4.2)64Bit版
The Google Plugin for Eclipse, for Eclipse 3.8/4.2 (Juno)

ROME 1.0
JDOM 1.1.3
( Xerces 2.11.0 )


(1)はじめに Introduction
JavaのRSSライブラリには、いろいろあるみたい。
There are many library for RSS-reading.
Java や Ruby の RSS/Atom feed ライブラリなど。HTMLからRSSを自動生成……したいなぁ。 (RSS – MemoWiki)

いくつか見てみたんだけど、RSSライブラリはあんまり活発な開発がされてない雰囲気。
どうも、RSSは、昔から使われていて仕様がかたまっているため、ライブラリの改修をする必要があんまり無いっぽい。
で、なんとなく情報量の多そうな、ROMEを使うことにした。
I search RSS-library. But some RSS-library is not active.
I think that RSS-format is so old, and changeless. So, it’s no need to modify RSS-library.


(2)ROME, JDOMのダウンロード  Downloading “ROME and JDOM”
こちらのサイトから、ROMEのダウンロードを行います。僕の場合、rome-1.0.jarをダウンロードしました。
You can download ROME from this site. I downloaded rome-1.0.jar.
Home – ROME – Confluence

ROMEはJDOMと依存関係があるため、こちらのサイトからJDOMもダウンロードします。
ROME have to use with JDOM. So, you should download JDOM from thi site.
JDOM

現時点でJDOM 2.0.5が最新版でしたが、こちらを使うとROMEがうまく動きませんでした。
なので、JDOM 1.1.3を使う必要があります。ダウンロードしたファイルjdom-1.1.3.zipの中のbuildフォルダに、jdom-1.1.3.jarがあるので、これをEclipseのライブラリに追加します。
“JDOM’s latest version 2.0.5” don’t work with ROME. You should use JDOM 1.1.3.
Download jdom-1.1.3.zip, then there is jdom-1.1.3.jar in “build” folder. And add jdom-1.1.3.jar to Eclipse-library.


(3)Xercesのダウンロード Downloading Xerces
こちらは、必要に応じて使うものになります。
単体のJavaアプリとして動かす際には必要ないですが、GAE上でROMEを動かそうとすると、以下のようなエラーが出ます。

Xerces dosn’t need, if you don’t make your Apps on GAE.
But if you use ROME on GAE, you would face this error.

java.lang.IllegalStateException: JDOM could not create a SAX parser
    at com.sun.syndication.io.WireFeedInput.createSAXBuilder(WireFeedInput.java:328)
    at com.sun.syndication.io.WireFeedInput.build(WireFeedInput.java:189)
    at com.sun.syndication.io.SyndFeedInput.build(SyndFeedInput.java:123)
   :   :
   :   :

原因は謎。こちらのサイトによると、Xercesが必要とのこと。
I don’t know Why this error has happen. According to these site, you need Xerces.
Romeが使えない – Google グループ
Issue 1367 – googleappengine – org.apache.xerces.parsers.SAXParser not available – Google App Engine – Google Project Hosting

そのため、こちらのサイトからXercesをダウンロードします。
You can download Xerces from this site.
The Apache Xerces™ Project – xerces.apache.org

ダウンロードしたファイル Xerces-J-bin.2.11.0.tar.gz の中にある、xercesImpl.jarと、xml-apis.jar をEclipseのライブラリに追加します。
Download Xerces-J-bin.2.11.0.tar.gz. In this file, you can get xercesImpl.jar and xml-apis.jar. Then you need to add these Jar-files into Eclipse-library.


(4)コーディング Coding
こちらのサイトを参考にしました。
This site will help you.
JavaのRSSライブラリ、ROMEを使ってみる
RSS・Atomフィードのためのライブラリ”ROME” – argius note
Kazuhiro’s Weblog: Javaで手軽にフィード(RSS、Atom)を扱うライブラリ 「ROME」

RSS取得時のコードは以下のような書き方になります。
You can get RSS with this code.

        String url = "http://jp.techcrunch.com/feed/";

        URL feedUrl = new URL(url);
        XmlReader reader = new XmlReader(feedUrl.openStream());
            SyndFeedInput input = new SyndFeedInput();
            SyndFeed feed = input.build(reader);
        reader.close();

上記では取得したデータが、feedの中に格納されています。
feed.getTitle()といった形で、個々のデータが取得できます。
RSSの1つ1つの記事は、feed.getEntries()でまとめて取得できます。List形式のデータになっています。

That code will give you RSS-data in  a variable “feed”.
You can get each a RSS-element using the method “feed.getTitle()”.
And you can get a RSS-entry using the method “feed.getEntries()”.

とりあえず、取得したデータをすべて表示してみることにしました。最終的なソースコードを以下にまとめて記載します。
I could show you each elements in RSS., If you use this code.

RomeSample3.java

import java.net.URL;
import java.util.List;

import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.XmlReader;

public class RomeSample3 {
    public static void main(String[] args) throws Exception {
        // String url = "http://rss.rssad.jp/rss/gihyo/feed/rss2";

        // String url = "http://rss.dailynews.yahoo.co.jp/fc/rss.xml";
        String url = "http://jp.techcrunch.com/feed/";
        // String url = "http://wired.jp/rssfeeder/";

        URL feedUrl = new URL(url);
        XmlReader reader = new XmlReader(feedUrl.openStream());
            SyndFeedInput input = new SyndFeedInput();
            SyndFeed feed = input.build(reader);
        reader.close();

        // ###########################################################
        System.out.println("----- Site Infomation ----------------------------------");
        System.out.println("SITE author : " + feed.getAuthor());
        System.out.println("SITE authors : " + feed.getAuthors());
        System.out.println("SITE categories : " + feed.getCategories());
        System.out.println("SITE contributors : " + feed.getContributors());
        System.out.println("SITE Copyright : " + feed.getCopyright());
        System.out.println("SITE description : " + feed.getDescription());
        System.out.println("SITE DescriptionEx : " + feed.getDescriptionEx());
        System.out.println("SITE encoding : " + feed.getEncoding());
        System.out.println("SITE feedType : " + feed.getFeedType());
        System.out.println("SITE ForeignMarkup : " + feed.getForeignMarkup());
        System.out.println("SITE image : " + feed.getImage());
        System.out.println("SITE language : " + feed.getLanguage());
        System.out.println("SITE link : " + feed.getLink());
        System.out.println("SITE links : " + feed.getLinks());
        // System.out.println("SITE Module : " + feed.getModule(url));
        // System.out.println("SITE Modules : " + feed.getModules());
        System.out.println("SITE getPublishedDate : " + feed.getPublishedDate());
        System.out.println("SITE getSupportedFeedTypes : " + feed.getSupportedFeedTypes());
        System.out.println("SITE title : " + feed.getTitle());
        System.out.println("SITE getTitleEx : " + feed.getTitleEx());
        System.out.println("SITE uri : " + feed.getUri());

        // ###########################################################
        List<SyndEntry> entries = feed.getEntries();

        Integer i = 1;
        for (SyndEntry entry : entries) {
            System.out.println("----- Entries No." + i + "----------------------------------");
            System.out.println("ENTRY Author : " + entry.getAuthor());
            System.out.println("ENTRY Authors : " + entry.getAuthors());
            System.out.println("ENTRY Categories : " + entry.getCategories());
            System.out.println("ENTRY Contents : " + entry.getContents());
            System.out.println("ENTRY Contributors : " + entry.getContributors());
            System.out.println("ENTRY Description : " + entry.getDescription());
            System.out.println("ENTRY Enclosures : " + entry.getEnclosures());
            System.out.println("ENTRY ForeignMarkup : " + entry.getForeignMarkup());
            System.out.println("ENTRY Link : " + entry.getLink());
            System.out.println("ENTRY Links : " + entry.getLinks());
            // System.out.println("ENTRY Module : " + entry.getModule(url));
            // System.out.println("ENTRY Modules : " + entry.getModules());
            System.out.println("ENTRY PublishedDate : " + entry.getPublishedDate());
            System.out.println("ENTRY Source : " + entry.getSource());
            System.out.println("ENTRY Title : " + entry.getTitle());
            System.out.println("ENTRY TitleEx : " + entry.getTitleEx());
            System.out.println("ENTRY UpdatedDate : " + entry.getUpdatedDate());
            System.out.println("ENTRY Uri : " + entry.getUri());
            System.out.println("ENTRY WireEntry : " + entry.getWireEntry());
            System.out.println("");

            i = i + 1;
        }
    }
}

これで、RSSの受信はできました。I could get RSS.

ほしい情報は、記事のタイトルと要約の部分になります。ソース上では以下の部分。
Title and Descriptin are important for me. I can get those using this method .

entry.getTitle()
entry.getDescription()

実際にRSS取得してみると、記事丸ごとが広告になっている場合と、要約の中に広告が埋まっている場合があります。
タイトルが”PR:”で始まっているものは、記事全体が広告になってる。これはタイトルの文字列をチェックするだけなので取り除くのは簡単です。
要約の中に広告が埋まっている場合は、以下のような形で、広告のリンクが埋まっています。

I got RSS-feed, then I found that there is some Ads in RSS. Some entry are ads, and some RSS-description has ads.
If “Title” start with “PR:” , RSS-entry is a advertisement. It’s easy to check “Title”.
But if RSS-description had ads, it’s difficult to remove ads.
This is sample that there are “link to ads” in description.

<p>前回紹介したように,gmtpに文字コード回りのパッチをあてることで,無事Plamo Linux環境とNexus 7の間でファイルをやりとりできるようになりました。しかしながら,あれこれ使っているとgmtpの欠点も目についてきました。
<img border="0" width="1" height="1" src="http://rss.rssad.jp/rss/artimg/47ghQF72z411/3184e1aa6c761b751f68c2613f8a3f55"/></p>
<br clear="all" /><a href="http://rss.rssad.jp/rss/ad/47ghQF72z411/pFjyc8QFo.Eh?type=1" target="_blank"><img src="http://rss.rssad.jp/rss/img/47ghQF72z411/pFjyc8QFo.Eh?type=1" border="0"/></a><br/>

この場合HTMLをParseする必要があるため、HTMLパーサのライブラリを使い、不要な部分のみを取り除くことにします。ここから先は、次回のエントリーで。
In this case, I have to parse HTML. So I will use HTML-parsing-library. And I will remove the link of ads.
I will show you that in next post.

GAEでSpringのValidation機能を使おうとしてハマった

GAE上でSpringMVCを動かしています。formからの入力にValidationを試してみました。で、どっぷりハマったので、とりあえず原因を速報。

“hibernate-validator 5.*” don’t work on GAE!!

————————————————————–

使っている環境

Java JDK1.6.0_35(64bit版)
Google App Engine SDK for Java 1.7.1
Eclipse IDE for Java EE Developers_ Juno (4.2)64Bit版
The Google Plugin for Eclipse, for Eclipse 3.8/4.2 (Juno)
Spring Framework 3.2.2.RELEASE
Commons Collections 3.2.1
Commons Lang 2.6
Commons Logging 1.1.2
Apache Velocity 1.7
hibernate-validator-4.3.0.Final ( + jboss-logging-3.1.0.CR2.jar, validation-api-1.0.0.GA.jar)


(1)NG編

まず、現時点での最新版である、
hibernate-validator 5.0.0.Final
を使ってみました。I try to use latest version “hibernate-validator 5.0.0.Final”.

ダウンロード先はこちら。
Downloads – JBoss Community

ダウンロードしたhibernate-validator-5.0.0.Final-dist.tar.gzの中にある、
hibernate-validator-5.0.0.Final.jar
validation-api-1.1.0.Final.jar
jboss-logging-3.1.1.GA.jar

をライブラリに追加して、アプリ起動、以下のエラーが出ました。NoClassFoundError has occured.

java.lang.NoClassDefFoundError: com/fasterxml/classmate/Filter

こちらのサイトから
SpringSource Enterprise Bundle Repository

FasterXML ClassMate 0.5.4をダウンロードして、com.springsource.com.fasterxml.classmate-0.5.4.jarをライブラリに追加しました。
アプリ起動してみると、またエラーが出ました。NoClassFoundError has occured again !

java.lang.ClassNotFoundException: de.odysseus.el.ExpressionFactoryImpl

そのため、今度はこちらのサイトから、
Java Unified Expression Language

juel-2.2.6をダウンロードしてきて、juel-impl-2.2.6.jarをライブラリに追加しました。
そうすると、アプリが正常に起動するようになりました。

しかし!

実際にValidation機能を作動させると、INTERNAL_SERVER_ERRORが発生。
このような謎のエラーが出てました。InternalServerError has occured !!

java.lang.ExceptionInInitializerError
    at org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator.interpolateExpression(ResourceBundleMessageInterpolator.java:227)
    at org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator.interpolateMessage(ResourceBundleMessageInterpolator.java:187)
    at org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator.interpolate(ResourceBundleMessageInterpolator.java:120)
   :   :
   :   :

Caused by: javax.el.ELException: Could not create expression factory instance
    at javax.el.ExpressionFactory.newInstance(ExpressionFactory.java:225)
    at javax.el.ExpressionFactory.newInstance(ExpressionFactory.java:183)
    at javax.el.ExpressionFactory.newInstance(ExpressionFactory.java:89)
    at org.hibernate.validator.internal.engine.messageinterpolation.InterpolationTerm.<clinit>(InterpolationTerm.java:60)
    ... 68 more
Caused by: java.security.AccessControlException: access denied (java.io.FilePermission C:\Program Files\Java\jdk1.6.0_35\jre\lib\el.properties read)
    at java.security.AccessControlContext.checkPermission(AccessControlContext.java:374)
    at java.security.AccessController.checkPermission(AccessController.java:546)
    at java.lang.SecurityManager.checkPermission(SecurityManager.java:532)
    at com.google.appengine.tools.development.DevAppServerFactory$CustomSecurityManager.checkPermission(DevAppServerFactory.java:283)
    at java.lang.SecurityManager.checkRead(SecurityManager.java:871)
    at java.io.File.exists(File.java:731)
    at de.odysseus.el.ExpressionFactoryImpl.loadDefaultProperties(ExpressionFactoryImpl.java:258)
    at de.odysseus.el.ExpressionFactoryImpl.loadProperties(ExpressionFactoryImpl.java:283)
    at de.odysseus.el.ExpressionFactoryImpl.<init>(ExpressionFactoryImpl.java:162)
    at de.odysseus.el.ExpressionFactoryImpl.<init>(ExpressionFactoryImpl.java:147)
   :   :
   :   :

el.propertiesとかいうファイルのことをいろいろ調べたものの、情報少なく、原因不明。

おそらく、de.odysseus.el.ExpressionFactoryImplがファイルを読み込もうとしている。で、GAEではファイルの読み書きは禁止されているのでエラーとなっている。
I think “juel-2.2.6” try to read el.properties. But, on GAE, it doesn’t permit to read files. So Error has occured. I don’t know Why “juel” try to read.

しかし、SpringのTutorialをみると、きちんと動いている!
But , according to “SpringTutorial”,  It’s work well !
Spring Security in Google App Engine | SpringSource Team Blog


(2)Success!!
最終的に分かったのは、どうもhibernate-validator-5.*からちょっとつくりが変わっているみたい。
I think that  hibernate-validator-5.* has changed any architecture.

hibernate-validator-4.3.0.Finalを使ったらうまく動くようになりました。
I try to use “hibernate-validator-4.3.0.Final”, it’s work well !!
Don’t use “hibernate-validator-5.*” on GAE !!  Use “hibernate-validator-4.3.0″

また、hibernate-validator-4.3.0を使った場合、FasterXML、juelは無くてもOKです。
You don’t need “FasterXML and juel”, when you use “hibernate-validator-4.3.0” on GAE.

Javaの使い方メモ(Interface versus Abstract Class)

たまたま読んだ記事が、ためになったのでざっくり訳してみることにした。
こちらの記事 → Java: Interface versus Abstract Class

<補足>~~~~~~~~
Interfaceの使い方とか、基本的なところはこちらのサイトがわかりやすかった

(参考) あぷり部屋 - Java入門講座(インターフェイスについて理解しよう!)
(参考) interfaceについて本気出して考えてみた – 都元ダイスケ IT-PRESS
(参考) Java: Are objects of the same type as the interface implemented?
~~~~~~~~~~~~

ここから先、翻訳内容です。


What’s the difference between an interface and an abstract class in Java?
interfaceと抽象クラスの違いは?

まずは、それぞれの定義をざっくり確認してみる。そのうえで違いを確認してみよう!

abstractなメソッドを持つクラスは、abstractと宣言しないといけない。abstractなメソッドというのは、「メソッドを定義するが、具体的な処理内容を記述しない」といった形のメソッド。

When to use abstract methods in Java?
抽象メソッドのつかいどころ。

「なんで抽象メソッドをつかいたいって思うか?」、ってことを以下のExampleで考えてみる。

/* the Figure class must be declared as abstract 
   because it contains an abstract method  */

public abstract class Figure
{
	/* because this is an abstract method the 
	   body will be blank  */
	public abstract float getArea();	
}

public class Circle extends Figure
{
	private float radius;

	public float getArea()
	{
		return (3.14 * (radius * 2)); 	
	}
}

public class Rectangle extends Figure
{
	private float length, width;

	public float getArea(Figure other)
	{
		return length * width;
	}
}

上記では、抽象メソッドgetArea()が定義されています。なので、Figureクラスは抽象メソッドを持っていることになり、abstractと宣言しないといけない。

Figureクラスを継承したCircleクラス、Rectangleクラスが定義されており、それぞれのクラスにはgetAreaメソッドが定義されています。

ここでのポイントは、「なぜ、Figureクラスの中でgetAreaメソッドを抽象メソッドとして定義したか?」ということ。getAreaメソッドでは、図形の面積を計算しています。

Figureクラスの中では、特定の図形を指定していません。そのため、Figureクラスの中ではgetAreaメソッドに具体的な計算内容を定義していません(定義できない)。つまり、これが、Figureクラスの中ではgetAreaメソッドを抽象クラスとして定義している理由ですね。

Figureクラスを継承する場合、以下の2パターンが考えられる。
・Figureクラスを継承したサブクラスでgetAreaメソッドを実装(オーバーライド)する。
・Figureクラスを継承したサブクラスでgetAreaメソッドを実装(オーバーライド)しない場合、そのサブクラスは抽象クラスとして宣言する。

A non abstract class is called a concrete class
抽象クラスでないものは、具象クラスという。

ということをふまえて、次に、「Interfaceって何か、interfaceと抽象クラスの違いは?」を考えてみる。

Java interface versus abstract class
Interface vs 抽象クラス

Interfaceなクラスではありません。ってところが、抽象クラスとの違いです。
Interfaceは、ほかのクラスに継承されて利用されるtype(型?)になります。

Interfaceを継承するクラスは、以下の2つの条件を満たす必要があります。
・クラスを定義するときに、”implements Interface_Name“といった形で、定義する必要があります。
・クラスの中で、Interfaceで定義されているすべてのメソッドを実装しておく必要があります。

以下のような”Dog”というInterfaceがあった場合に、

public interface Dog
{
    public boolean Barks();
    public boolean isGoldenRetriever();
}

このInterfaceを継承したクラスは、以下のような感じになる。

public class SomeClass implements Dog
{
    public boolean Barks{
    // method definition here
    }

    public boolean isGoldenRetriever{
    // method definition here
    }
}

ここまでの内容で、Interfaceと抽象クラスについて、基本的なことを理解できたと思います。というわけで、それぞれの違いについて見ていきたいと思います。

Abstract classes and inheritance
抽象クラスと継承

(1)抽象クラスは、ほかのクラスに継承されるものになります。あるクラスが継承されるということは、互いのクラスに強い関連性(依存関係?)が生じるということになります。

たとえば、抽象クラスとしてCanineクラスが定義されているとします。Canineクラスを継承したクラス(DogとかWolfクラス)は、Canineファミリーに所属すべきです。「すべき」ということは、開発者がメンテナンス・維持管理していく、ってことになる。

それに対しInterfaceの場合、Interface本体と、「InterfaceをImplementしたクラス」との関係は強くない。
たとえば、Houseというクラスがあったとします。Houseクラスは、AirConditioningというInterfaceをImplementしているとします。この場合、HouseにAirConditioning(空調設備)があるかどうかはそれほど強い関係性ではありません。

逆に「Houseクラスを継承したApartmentクラスと、Houseクラス」といった関係性のほうがはるかに強いつながりを持っています。
なぜかというと、ApartmentはHouseの中の1つに属していることから関連が強いわけです。そのため、クラスの継承をつかってきっちり定義するべきです。この点がInterfaceとの違いになってきます。

まとめると、
抽象クラスとそれを継承したクラスの間に、つよい関係性が考えられる、という場合、「抽象クラスをつかうべき」です。抽象クラスは、”継承”により、クラス間が密接につながります。つまり関係性が強くなる。
それに対し、Interfaceの場合、Interfaceと、それをImplementしたクラスの間に、つよい関係性は必要ありません。

Interfaces are a good substitute for multiple inheritance
Interfaceは、多重継承の代わりになる。

(2)Javaは多重継承を許可していません。Javaでは、クラスは1つのクラスしか継承できません。抽象クラスであろうがそうでなかろうが、継承できるのは1つだけです。
ですが、Interfaceは複数Implementできます。そのため、多重継承の代替になりえます。

抽象クラスとInterfaceの主な違いの1つとしては、「Javaのクラスは、抽象クラスを1つだけしか継承できないが、Interfaceは複数Implementできる」、と言えます。

Abstract classes can have some implementation code
抽象クラスは、実装コードを持てる。

(3)抽象クラスは、メソッドに処理手順を書き込めます。抽象クラスは、具体的な処理を記載したメソッドを持つことができます。また、抽象クラスは、コンストラクタ・インスタンス変数も持つことができます。
ですが、Interfaceはメソッドに処理手順を書き込めません。メソッドの名前だけ定義できます。InterfaceをImplementするクラスは、Interfaceで定義されているメソッドについて、すべて実装する必要があります。

When to use abstract class and interface in Java
抽象クラス、Interfaceのつかいどころ。

抽象クラス、Interfaceをつかうときのガイドラインを以下に示します。

1.次のように考えているなら、抽象クラスが適している。
「共通のBaseとなるクラスを用意して、それを継承したクラスを使う」といった形で、”継承”を使っていこう!

2.Publicな使い方をしたくないなら、抽象クラスをつかうのがよい。Interfaceで定義するメソッドはPublicでないといけない。

3.将来的にメソッドの追加が予想される場合、抽象クラスを使うほうがよい。Interfaceにメソッドを追加すると、そのInterfaceをImplementしているクラスすべてに、メソッド追加しないといけなくなる。おおさわぎになるかも!?

4.さしあたり、APIに変更が入らないという場合、Interfaceをつかうのがよい。

5.多重継承のようなことを行いたいと思っている場合、Interfaceを使うのがよい。複数のInterfaceをImplementすることができる。

Javaの使い方メモ(Downcast, Generics)

Javaの勉強中です。
Tutorialとか、Examplesにあるコードを見ていると、

String str = (String) list.get(0);
とか、
List<String> list = new ArrayList<String>();

といった記述を目にすることがあって、「この丸カッコ・三角カッコで囲まれているものは何だろう。」と思ったものの、「型を指定しているんだろうなぁ、きっと。」と考えてスルーしてきました。
が、いまさらながら、きちんと調べてみました。

使っている環境
Java JDK1.6.0_35(64bit版)


(1)DowncastとGenerics
Genericsの機能がJavaになかった頃、Listに文字列を追加して、それを取出す場合、このようにしていた。
(参考) Javaジェネリクス再入門 – プログラマーの脳みそ

List list = new ArrayList();
list.add("hello!");
String str = (String) list.get(0);

ArrayListに追加される要素は、Object型となっている。そのため、list.get(0)として、要素を取出したとき中身がStringであったとしても、要素の型はObject型となっている。

このままの状態だと、String型の変数strに値を格納することができない。そのため、
(String) list.get(0)

として、Downcastしている。(上位の型(Object型)を下位の型(String型)に変換している)
Downcastしたときの問題点として、「コンパイル時にエラーが出ないが、アプリ実行時にエラーが発生することがある」という問題がある。
上記の例の場合、listの要素にString以外のものが入っていた場合、「コンパイルは通るが実行時エラー」となる。ClassCastExceptionが発生するらしい。
このClassCastExceptionを解決するにはは、listにどんなデータが格納されていたかを調査する必要があり、デバッグがとても大変らしい。

そこで、現在ではJavaのGenerics機能をつかって、以下のように書いている。

List<String> list = new ArrayList<String>();
list.add("hello!");
String str = list.get(0);

List<String>と書くことで、listにString型がバインドされる。これにより、listに格納される要素はString型のみとなる。また、list.getするとString型が返るようになる。


(2)Genericsとは?
クラス定義でGenericsを使える。
(参考)ジェネリックスとは?なぜジェネリックスなのか – Java 入門

たとえば以下のようなクラスBoxを定義すると、

public class Box<T> {
    T o;

    public Box(T o){
        this.o = o;
    }
    public T get(){
        return o;
    }
}

クラスを使うときに、Box<Integer>、Box<String>といった形で指定することにより、それぞれの型用の Box クラスを用意できるようになる。以下のような感じでBox<Integer>を使うと、getしたときの返り値もInteger型になる。

Box<Integer> b = new Box<Integer>(new Integer(123));
Integer i = b.get();
System.out.println(i);

Genericsを使うことにより、Integer用・String用といったクラスを別々に定義する必要がなくなり、汎用化できる。僕に使いこなせるかどうかは、わからないけど(笑)


(3)もっとGenerics
メソッドにも使える。
(参考)Java Generics Tutorial

public class GenericMethodTest
{
   // generic method printArray                         
   public static < E > void printArray( E[] inputArray )
   {
      // Display array elements              
         for ( E element : inputArray ){        
            System.out.printf( "%s ", element );
         }
         System.out.println();
    }

    public static void main( String args[] )
    {
        // Create arrays of Integer, Double and Character
        Integer[] intArray = { 1, 2, 3, 4, 5 };
        Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
        Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };

        System.out.println( "Array integerArray contains:" );
        printArray( intArray  ); // pass an Integer array

        System.out.println( "\nArray doubleArray contains:" );
        printArray( doubleArray ); // pass a Double array

        System.out.println( "\nArray characterArray contains:" );
        printArray( charArray ); // pass a Character array
    } 
}

printArrayメソッドでは、渡された配列の内容を表示しています。Genericsをつかうことで、int型の配列・double型の配列・String型の配列に対応できるようになっています。
実行すると以下のようになる。

Array integerArray contains:
1 2 3 4 5 6

Array doubleArray contains:
1.1 2.2 3.3 4.4 

Array characterArray contains:
H E L L O

(4)Downcastの使い道
どうしてもDowncastを使う場合、ClassCastExceptionを避けるために、instanceof演算子などを使って型のチェックをしておくべき。
(参考)8章:継承
(参考)強く型付けされているJavaの理解に必修の“型変換” (2/3) – @IT

    if( obj instanceof String )
        str1 = (String)obj;

でも、「そもそもDowncastをどういうときに使うべきか?」っていう素朴な疑問がわいてくる。つかいどころ、ってどこ?
これについては、こちらのサイトに例がありました。
(参考)Downcasting in Java

オブジェクトとオブジェクトの型を比較したいときにDowncastを使うと便利らしい。

public class Person
{
  private String name;
  private int age;

  public boolean equals(Object anObject)
  {
     if (anObject == null)
         return false;

     /* The object being passed in is checked
         to see it's class type which is then compared
         to the class type of the current class.  If they
         are not equal then it returns false
     */

     else if (getClass( ) != anObject.getClass())
         return false;

     else
     {
        /* 
         this is a downcast since the Object class
         is always at the very top of the inheritance tree
         and all classes derive either directly or indirectly 
         from Object:
        */
        Person aPerson = (Person) anObject;
        return ( name.equals(aPerson.name) 
                && (age == aPerson.age));
     }
  }

}

上記の例では、Personクラスのequals()メソッドでDowncastを使っています。異なる2つのオブジェクト(自分自身とanObject)のnameとageを比較しています。

想定される使用イメージはこんな感じ。
・Personクラスを継承したクラスとして、Japanese, American, Chineseといったクラスがあるとします。
・で、それぞれのクラスを比較したい。

Personクラスのequals()メソッドでは、引数として様々なクラスを引き受ける必要があるため、引数としてObject型の変数anObjectを使っています。Personクラスと比較するときに、型を合わせるために、Downcastしています。

今回の例では、ClassCastExceptionを避けるために、instanceof演算子ではなくgetClassを使っています。なのでJapaneseどうし、Americanどうしであった場合に、Downcastして比較することになりますね。

なお、instanceof演算子とgetClassの微妙な違いが気になったので、以下に引用しておきます。

~~~~~~~~~~~~~~~~~~
(参考)4章 継承
instanceof演算子について(以下、引用)
<オブジェクト変数>に代入されているインスタンスのクラスが<クラス名>と一致すればtrueを返します。また、インスタンスが<クラス名>クラスを継承している場合も、trueが返ります。

ObjectクラスのgetClass()を用いてインスタンスobjがクラスAのインスタンスであるか調べる場合、obj.getClass() == A.classと比較することでできます。
もし、インスタンスobjがクラスAを継承したクラスA’のインスタンスである場合、上記の比較構文はfalseを返します。instanceofであればtrueになることに注意してください。
~~~~~~~~~~~~~~~~~~

GAEでSpringMVCを動かしてみる4(続 Spring Security を試してみる)

前回のエントリー(GAEでSpringMVCを動かしてみる3(Spring Security を試してみる) | Walk on apps.)からの続きです。

SpringSecurityを利用して、ログイン・ログアウト機能を試しています。
前回は、「とりあえず、まずは動かしてみる、」ってことをやってみました。
今回は、パスワード暗号化や、ユーザごとのアクセス制御、HTTPS対応、とかを追加して「もうちょっと、きちんと動かしてみる」、つもりです。

前回のエントリーまでは、ソースコードをBlog上にまるごと貼り付けていたんですが、大変なのでGitHubで公開することにしました。
GitHubからDownload

Blogでは、ポイントに絞って書いていきます。


(6)もうちょっと、きちんと動かしてみる
次のようなことを、設定してみようと思います。
・パスワードの暗号化
・アクセス制御
・ログインユーザ名の取得
・オリジナルのLoginページの作成
・Logout時の設定
・通信の暗号化(HTTPS設定)
・日本語の文字化け対応

・パスワードの暗号化
StandardPasswordEncoderを利用して、パスワードを暗号化します。SpringSecurityのチュートリアルを参考にしました。「Using encoded passwords」のところに設定例が載っています。
Spring Security — Tutorial

マニュアルによると、StandardPasswordEncoderは、SHA-256でハッシュ化を行うようです。あと、saltとして、ランダムな8Byteデータを付与してくれる。
25. Spring Security Crypto Module

SpringSecurityのBean定義ファイルには、以下のように書きました。ユーザを2つ(admin, bob)定義しています。パスワードは、ユーザ名と同じにしています。

spring-security-dispatcher.xml への追記内容

    <beans:bean id="encoder" class="org.springframework.security.crypto.password.StandardPasswordEncoder"/>

    <authentication-manager>
        <authentication-provider>
            <password-encoder ref="encoder"/>
            <user-service>
                <user name="admin" password="d4c4f5f190abf98b0d5224b549a1f2645acf975c75ec5d2d51895b0a7ca35949fc33576cc8294769" authorities="supervisor, user" />
                <user name="bob" password="cb60b076bebc371a3e5f6097399a82eb65948513639f4b090e70e2d49b6808fa3b02aa9bcd25668f" authorities="user" />
            </user-service>
        </authentication-provider>
    </authentication-manager>

ハッシュ値の計算方法は、StandardPasswordEncoderのencodeメソッドを使います。チュートリアルでは、scalaのインタプリタを使っていますが、僕は地道にJavaのコードを書きました。以下のようにして、ハッシュ値を計算できます。

File名:PasswordEncode.java

import org.springframework.security.crypto.password.StandardPasswordEncoder;

public class PasswordEncode {
        public static void main(String[] args) throws Exception {
            StandardPasswordEncoder encoder = new StandardPasswordEncoder();
            System.out.println(encoder.encode("admin"));
            System.out.println(encoder.encode("bob"));
        }
}

・アクセス制御
SpringSecurityのBean定義ファイルで、use-expressions=”true”の設定をして、アクセス制御の設定をします。
これも、SpringSecurityのチュートリアルを参考にしました。
Spring Security — Tutorial

以下のような権限を、設定してみることにしました。
・index.htmlと、ログイン・ログアウト画面には、全員に許可。
・コンテキストパス「/velocity/admin」はadminだけ許可。
・コンテキストパス「/velocity/user」は、admin, bobに許可。
・その他のパスは、アクセス禁止。

spring-security-dispatcher.xml への追記内容は、こんな感じ。

<http use-expressions="true">
    <intercept-url pattern="/index.html" access="permitAll" />
    <intercept-url pattern="/velocity/login" access="permitAll"/>
    <intercept-url pattern="/velocity/loginfailed" access="permitAll" />
    <intercept-url pattern="/velocity/logout" access="permitAll" />

    <intercept-url pattern="/velocity/admin/**" access="hasRole('supervisor')" />
    <intercept-url pattern="/velocity/user/**" access="isAuthenticated()" />
    <intercept-url pattern="/**" access="denyAll" />

   :    :
   (中略)

</http>

access=”permitAll”といった、権限の書き方一覧は、以下のマニュアルの「16.1.1 Common Built-In Expressions」「16.2 Web Security Expressions」に載っていました。
16. Expression-Based Access Control
上記マニュアルによると、permitAllといった書き方は、WebSecurityExpressionRootクラスで定義されているとのこと。一応JavaDocも見てみた。
WebSecurityExpressionRoot (Spring Security 3.2.0.M1 API)

・ログインユーザ名の取得
現在ログインしているユーザ名の取得を行い、ブラウザ画面に表示できるようにします。
Controllerのコードを修正します。こちらのサイトを参考にしました。
Get current logged in username in Spring Security

ControllerのメソッドにPrincipalインターフェースを追加して、それを利用してユーザ名を取得しています。
修正ファイル:VelocityController.java

    (中略)
   :    :

@Controller
@RequestMapping("/velocity")
public class VelocityController {

    //DI via Spring
    String message;

    public void setMessage(String message) {
        this.message = message;
    }

    @RequestMapping(value="/{path}", method = RequestMethod.GET)
    public String getMovie(@PathVariable String path, ModelMap model, Principal principal) {

        model.addAttribute("path", path);
        model.addAttribute("message", this.message);

        String name = principal.getName(); //get logged in username
        model.addAttribute("username", name);

        //return to .vm page, configured in mvc-dispatcher-servlet.xml, view resolver
        return "sample";
    }

   :    :
   (中略)

参考にしたサイトによると、ほかにも2つやり方があるらしく、ちょっと書き方変えるだけなので、参考に載せておきます。

まず、「Authentication.getName()」を使うパターン。ControllerのメソッドにPrincipalインターフェースを追加しなくても動きます。

   :  (中略)  :
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
   :  (中略)  :

    @RequestMapping(value="/{path}", method = RequestMethod.GET)
    public String getMovie(@PathVariable String path, ModelMap model) {

        model.addAttribute("path", path);
        model.addAttribute("message", this.message);

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String name = auth.getName(); //get logged in username
        model.addAttribute("username", name);

        //return to .vm page, configured in mvc-dispatcher-servlet.xml, view resolver
        return "sample";
    }
   :    :
   (中略)

次に「User.getUsername()」を使うパターン。こちらも、ControllerのメソッドにPrincipalインターフェースを追加しなくても動きます。

   :  (中略)  :

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
   :  (中略)  :

    @RequestMapping(value="/{path}", method = RequestMethod.GET)
    public String getMovie(@PathVariable String path, ModelMap model) {

        model.addAttribute("path", path);
        model.addAttribute("message", this.message);

        User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String name = user.getUsername(); //get logged in username
        model.addAttribute("username", name);

        //return to .vm page, configured in mvc-dispatcher-servlet.xml, view resolver
        return "sample";
    }
   :    :
   (中略)

・オリジナルのLoginページの作成
こちらのサイトを参考にしました。
Spring Security form login using database
このサイトからダウンロードしたサンプルに、ログイン画面のサンプルがあります。

まず、SpringSecurityのBean定義ファイルに、form-login設定をします。

login-pageでは、ログイン画面を指定します。authentication-failure-urlでは、認証エラー時に表示する画面を指定します。
default-target-urlは、リダイレクト先が指定されていないときに、ログイン後に表示する画面を指定します。

認証が必要な画面/velocity/userにアクセスするとログイン画面/velocity/loginが表示されます。ログイン完了すると /velocity/userにリダイレクトされます。ですが、ログイン画面/velocity/loginに直接アクセスする場合もあります。このとき は、ログイン完了後にリダイレクトする宛先がありません。こういったときに、default-target-urlで指定された画面が表示されます。

spring-security-dispatcher.xml への追記内容

<http use-expressions="true">
   :  (中略)  :
    <form-login login-page="/velocity/login" default-target-url="/index.html"
        authentication-failure-url="/velocity/loginfailed" />
   :  (中略)  :
</http>

次に、Controllerのメソッドにログイン画面への遷移を追加します。
ログイン画面/velocity/loginと、認証エラー画面/velocity/loginfailedは、同じHTMLテンプレートを用いています。認証エラー時は、error変数にtrueを設定し、テンプレートに返しています。

修正ファイル:VelocityController.java

:  (中略)  :
@Controller
@RequestMapping("/velocity")
public class VelocityController {

:  (中略)  :
    @RequestMapping(value="/login", method = RequestMethod.GET)
    public String login(ModelMap model) {

        return "login";
    }

    @RequestMapping(value="/loginfailed", method = RequestMethod.GET)
    public String loginerror(ModelMap model) {

        model.addAttribute("error", "true");
        return "login";
    }
:  (中略)  :

}

次にログイン画面のテンプレートを作成します。
認証エラー時に、SPRING_SECURITY_LAST_EXCEPTION.messageにエラー内容が入るみたいなので、認証エラー時はそれを表示するようにしています。

作成したFile:login.vm

<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>Login Page</title>

<style>
.errorblock {
    color: #ff0000;
    background-color: #ffEEEE;
    border: 3px solid #ff0000;
    padding: 8px;
    margin: 16px;
}
</style>

</head>

<body onload='document.f.j_username.focus();'>
    <h3>Login with Username and Password ログインテスト</h3>

    #if(${error}=="true")
        <div>
            Your login attempt was not successful, try again.<br /> Caused :
            $SPRING_SECURITY_LAST_EXCEPTION.message
        </div>
    #end

    <form name='f' action="j_spring_security_check"
        method='POST'>

        <table>
            <tr>
                <td>User:</td>
                <td><input type='text' name='j_username' value=''>
                </td>
            </tr>
            <tr>
                <td>Password:</td>
                <td><input type='password' name='j_password' />
                </td>
            </tr>
            <tr>
                <td colspan='2'><input name="submit" type="submit"
                    value="submit" />
                </td>
            </tr>
            <tr>
                <td colspan='2'><input name="reset" type="reset" />
                </td>
            </tr>
        </table>

    </form>
</body>
</html>

「認証エラー時に、SPRING_SECURITY_LAST_EXCEPTION.messageにエラー内容が入る」と書きましたが、その値がApacheVelocity側に引き渡されるように、VelocityViewResolverクラスへの設定追加が必要です。

Bean定義ファイルspring-security-dispatcher.xml の設定修正し、exposeSessionAttributesをtrueに設定します。

   :  (中略)  :
    <bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="contentType" value="text/html;charset=UTF-8" />
        <property name="cache" value="true"/>
        <property name="prefix" value=""/>
        <property name="suffix" value=".vm"/>
        <property name="exposeSessionAttributes" value="true"/>
    </bean>
   :  (中略)  :

・Logout時の設定

まず、SpringSecurityのBean定義ファイルに、logout設定をします。

logout-success-urlで、ログアウト画面を指定します。delete-cookiesでログアウト時にJSESSIONIDをcookieから削除するようにしています。

spring-security-dispatcher.xml への追記内容

<http use-expressions="true">
:  (中略)  :
    <logout logout-success-url="/velocity/logout" delete-cookies="JSESSIONID"/>

</http>

次に、Controllerのメソッドにログアウト画面への遷移を追加します。

修正ファイル:VelocityController.java

:  (中略)  :
@Controller
@RequestMapping("/velocity")
public class VelocityController {

    //DI via Spring
    String message;

    public void setMessage(String message) {
        this.message = message;
    }

:  (中略)  :

    @RequestMapping(value="/logout", method = RequestMethod.GET)
    public String logout(ModelMap model) {

        model.addAttribute("message", this.message);
        return "logout";
    }

}

次にログアウト画面のテンプレートを作成します。

作成したFile:logout.vm

<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>Hello App Engine</title>
  </head>

  <body>

    message: ${message}<br/>
    <p>Logoutしました。</p>
  </body>
</html>

・通信の暗号化(HTTPS設定)
SpringSecurityで特定のURLに対してHTTPSを要求する設定ができます。ただし、HTTPS自体をSpringSecurityで制御するわけでないので、HTTPSの入り口は、APサーバ等で用意しておく必要があります。

そのため、まず、GoogleAppEngine側でHTTPSの入り口を用意する必要があります。GAEでHTTPSを使う場合、web.xmlにsecurity-constraint設定を追加すると利用できます。
配備記述子: web.xml – Google App Engine — Google Developers

注意点としては、Google App Engine SDKでは、開発環境ではHTTP接続、GAE上ではHTTPS接続になります。
SpringSecurityでHTTPSを要求する設定をした場合、開発環境でもHTTPSが使えるようにしておかないと、アクセスができません。そのため、僕の場合、HTTPS関連設定はGAE側にすべて任せることにします。

web.xmlに、security-constraint設定を追加します。

  :  (中略)  :
    <!-- GAE Setting -->
    <security-constraint>
        <web-resource-collection>
            <url-pattern>/velocity/login*</url-pattern>
            <url-pattern>/velocity/loginfailed*</url-pattern>
            <url-pattern>/velocity/logout*</url-pattern>
        </web-resource-collection>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>

    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
  :  (中略)  :

参考までに、SpringSecurityで設定する場合の内容を書いておきます。
マニュアルはこちら。3. Security Namespace Configuration

requires-channelでプロトコルを指定すると設定できます。
port-mappingは必要に応じて設定します。開発環境などで、通常のポートが使えないときに指定します。

  <http>
    <intercept-url pattern="/secure/**" access="ROLE_USER" requires-channel="https"/>
    <intercept-url pattern="/**" access="ROLE_USER" requires-channel="any"/>
    ...

    <port-mappings>
      <port-mapping http="9080" https="9443"/>
    </port-mappings>
    ...
  </http>

・日本語の文字化け対応
HTMLテンプレートに日本語があると、ブラウザで表示させたときに、文字化けがおきます。
そのため、Bean定義ファイルと、web.xmlに設定追加しておく必要があります。
こちらのサイトを参考にしました Spring-MVCのビューにVelocityを使いたい – PukiWiki

今回はUTF-8に統一することにします。
まず、Bean定義ファイルspring-security-dispatcher.xmlに設定追加します。

VelocityConfigurerクラスのinput.encoding、output.encodingにUTF-8を設定。
→ たぶん、テンプレートファイル読込と、HTML出力の設定だと思う

VelocityViewResolverクラスの、contentTypeにUTF-8を設定。
→ ブラウザに返却するContent-Typeを設定してる。HTTPヘッダですかね。

 
   :  (中略)  :
    <bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
        <property name="resourceLoaderPath" value="/WEB-INF/view/"/>

        <property name="velocityProperties">
            <props>
            <prop key="input.encoding">UTF-8</prop>
            <prop key="output.encoding">UTF-8</prop>
            </props>
        </property>
    </bean>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
        <property name="contentType" value="text/html;charset=UTF-8" />
        <property name="cache" value="true"/>
        <property name="prefix" value=""/>
        <property name="suffix" value=".vm"/>
        <property name="exposeSessionAttributes" value="true"/>
    </bean>
   :  (中略)  :

次にweb.xmlに設定追加します。

CharacterEncodingFilterクラスで、encodingにUTF-8を設定します。
→HTMLフォームからの入力された文字を、UTF-8にしてサーバ側で処理することになります。

   :  (中略)  :
    <!-- UTF-8 Setting -->
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
   :  (中略)  :

上記設定には、forceEncodingというパラメータもあるみたいだけど、今回は設定しませんでした。

   :  (中略)  :
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
   :  (中略)  :

(7)アプリ起動
GAE上で動かしてみました。
コンテキストパス/velocity/user にhttpでアクセスすると、ログイン画面にリダイレクトされます。httpsになってます。
spring4_01

bobでログインすると、画面が返ってきました。httpにもどっています。
ユーザ名bobが、画面に表示されています。
spring4_02

bobユーザのままで、/velocity/admin にアクセスすると、アクセス拒否されます。
spring4_03

/velocity/user 画面でログアウトをクリックすると、ログアウトします。httpsになっています。
spring4_04

ログイン画面でパスワードを間違えると、エラーメッセージが表示されます。
spring4_05