Home

sinsengumi血風録

CompassをMavenから実行できるプラグインを作った

CSS を効率よく書こうみたいなフレームワーク(と言うのか不明ですが)が流行ってて、LESS とか Sass とか色々あります。

この中で自分が結構いいなぁと思ったものに、Compass (Sass の拡張)というものがあって、ちょろちょろと試したりしているんですが、こういう CSS フレームワークっていうのは、静的にコンパイルする仕組みになっていて、アプリケーション開発とかに組み込む場合は、やっぱりビルドプロセスにうまく組み込みたいなぁと思うのが普通です(手動でやったら絶対ミスりますしね)

ということで、Maven から Compass を実行できるプラグインを作ってみました。

maven-compass-plugin
https://github.com/sinsengumi/maven-compass-plugin

README にも書いてますが、実行には Compass へのパスが通っている必要があります。
こういう外部プログラムを起動するタイプのやつはどうするのがベストプラクティスなんですかね。そこら辺のお作法がよくわからなかったので、直接コマンドを叩いてますけど。
(ちなみに LESS の maven プラグインは JRuby 経由で実行する形になってたけど)

Mavenリポジトリで提供されていないサードパーティJarをどうするか

  • Posted by: sinsengumi
  • 2012年12月16日 11:52 AM
  • Java
  • |

Maven のセントラルリポジトリやリムーブリポジトリ(※)で提供されていないサードパーティ Jar や俺俺ライブラリを pom.xml 上でどう管理するかという話です。
管理の仕方によって、開発者や管理者(環境構築をするようなアーキテクトっぽい人)の仕事が変わってくると思います。

http://daipresents.com/2009/maven2_internal_repository_webdav/

方法としては、主に以下の3つがあると思います。

  1. ローカルリポジトリに Jar をインストールする
  2. インターナルリポジトリを立てる
  3. system スコープを使う

1. ローカルリポジトリに Jar をインストールする

Jar ファイルをローカルのリポジトリに手動でインストールする方法。
以下のコマンドを叩くことで、自分のローカルリポジトリに Jar ファイルがインストールされる。

mvn install:install-file -Dfile=jarまでの絶対パス -DgroupId=<group-id> -DartifactId=<artifact-id> -Dversion=バージョン -Dpackaging=jar -DgeneratePom=true

メリット

  • 特になし?

デメリット

  • 開発者全員が同じコマンドを叩く必要がある
  • ライブラリのバージョンアップ時にも同じ作業が必要

2. インターナルリポジトリを立てる

開発用に新たなリポジトリサーバを立てる。いわゆる社内リポジトリ。
やり方は省略。

http://daipresents.com/2009/maven2_internal_repository_webdav/

メリット

  • 開発者自体は特別な操作を必要としない

デメリット

  • インターナルリポジトリの構築が手間
  • インターナルリポジトリがネットワーク経由でアクセス出来ないと環境構築できない(ソースだけあってもビルドできない)

3. system スコープを使う

ライブラリを system スコープのライブラリとして定義して、Jar ファイル自体はプロジェクト内で持つという方法です。
pom.xml には環境依存しないように systemPath を ${basedir} 等を使って定義する。。

<dependency>
	<groupId>net.sinsengumi</groupId>
	<artifactId>ThirdPartyJar</artifactId>
	<version>1.0</version>
	<scope>system</scope>
	<systemPath>${basedir}/lib/ThirdPartyJar.jar</systemPath>
</dependency>

メリット

  • 開発者は特別な操作を必要としない

デメリット

  • ライブラリをプロジェクト内で持つ必要がある(バージョン管理システムに含めなければならなかったりして、ファイルサイズが大きくなる)

※Webアプリケーションについて

Webアプリケーションの場合、WEB-INF/lib に Jar ファイルを置くことで自動的にクラスパスに含まれるので、ここに配置してしまうという手もありますが、pom.xml に依存性を書く必要がなくなるので、このライブラリだけ pom.xml に書かれてないみたいな気持ち悪さがあります。
そして、pom.xml に書かれてないと、mvn test 時にクラスパスが通らないという問題もあります。

あと、ライブラリをプロジェクト内で持つ必要がある、というデメリットも当然あります。

まとめ

方法としては、2. か 3. のどちらかかなぁと思います。
自分は管理の簡便性を求めて、3. でやっています。
プロジェクトの規模感で 2. でやる方法もありかなぁと思います。

HttpServletRequestWrapperを使って、HTTPリクエストを改変する

OpenAMは、認証済のユーザー情報をHTTPリクエストヘッダーに載せてくれるんですが、Servletでそれってどうやって実現してるのかなーと思って、調べたら HttpServletRequestWrapper クラスを使うようです。

使い方ですが、HttpServletRequestWrapper は、HttpServletRequestインタフェースを実装しているので、HttpServletRequestインタフェースのメソッドをオーバーライドして独自の実装を定義します。
http://mergedoc.sourceforge.jp/tomcat-servletapi-5-ja/javax/servlet/http/HttpServletRequestWrapper.html

今は、すべてのリクエストのヘッダーに情報を付与しようとしているので、getHeader辺りをオーバーライドします。

package org.sample;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class HeaderAddedHttpServletRequest extends HttpServletRequestWrapper {

    private Map<String, String> addHeaders;

    public HeaderAddedHttpServletRequest(HttpServletRequest request, Map<String, String> addHeaders) {
        super(request);

        if (addHeaders == null) {
            this.addHeaders = new HashMap<String, String>();
        } else {
            this.addHeaders = addHeaders;
        }
    }

    @Override
    public String getHeader(String name) {
        String header = super.getHeader(name);
        if (header != null) {
            return header;
        }

        header = addHeaders.get(name);
        if (header != null) {
            return header;
        }

        return null;
    }

    @Override
    public Enumeration<String> getHeaderNames() {
        List<String> newHeaderNames = new ArrayList<String>();

        Enumeration<String> currentHeaderNames = super.getHeaderNames();
        while (currentHeaderNames.hasMoreElements()) {
            newHeaderNames.add(currentHeaderNames.nextElement());
        }

        for (Entry<String, String> e : addHeaders.entrySet()) {
            newHeaderNames.add(e.getKey());
        }

        return Collections.enumeration(newHeaderNames);
    }
}

で、すべてのリクエストがこのWrapperクラスを使うようにFilterを定義します。

package org.sample;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;

@WebFilter("/*")
public class AddHeaderFilter implements Filter {

    @Override
    public void init(FilterConfig fConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        Map<String, String> addHeaders = new HashMap<String, String>();
        addHeaders.put("X-USER-ID", "HIJIKATA");
        addHeaders.put("X-USER-NAME", "土方歳三");

        chain.doFilter(new HeaderAddedHttpServletRequest((HttpServletRequest) request, addHeaders), response);
    }
}

これで、HTTPリクエストが飛んできたら自動でヘッダーが付与サれてます。

package org.sample;

import java.io.IOException;
import java.util.Enumeration;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/SampleServlet")
public class SampleServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            String headerValue = request.getHeader(headerName);

            System.out.println(headerName + "=" + headerValue);
        }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
    }
}
host=localhost:8080
connection=keep-alive
user-agent=Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.152 Safari/535.19
accept=text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
accept-encoding=gzip,deflate,sdch
accept-language=ja,en-US;q=0.8,en;q=0.6
accept-charset=Shift_JIS,utf-8;q=0.7,*;q=0.3
X-USER-ID=HIJIKATA
X-USER-NAME=土方歳三

同様の事はレスポンスでもできます。

シンプルな HTTP Server 書いた

以前の記事の続きということで、シンプルな HTTP Server を書いた。

classファイル一つで動くというお手軽サーバー。マルチスレッド?自分しか使わないからいいんですよ。
お手軽に使いたいのでデフォルトパッケージ。

java SimpleHttpServer "C:\htdocs" 81
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;

public class SimpleHttpServer {

    private static String requestPath;

    private static Map<String, String> CONTENT_TYPES = new HashMap<String, String>();

    static {
        CONTENT_TYPES.put("html", "text/html");
        CONTENT_TYPES.put("htm", "text/html");
        CONTENT_TYPES.put("css", "text/css");
        CONTENT_TYPES.put("js", "text/javascript");
        CONTENT_TYPES.put("jpg", "image/jpeg");
        CONTENT_TYPES.put("jpeg", "image/jpeg");
        CONTENT_TYPES.put("png", "image/png");
        CONTENT_TYPES.put("gif", "image/gif");
        CONTENT_TYPES.put("pdf", "application/pdf");
        CONTENT_TYPES.put("txt", "text/plain");
        CONTENT_TYPES.put("xml", "text/xml");
        CONTENT_TYPES.put("zip", "application/zip");
        CONTENT_TYPES.put("exe", "application/octet-stream");
    }

    public static void main(String[] args) throws IOException {
        String documentRoot = args[0];
        int port = Integer.parseInt(args[1]);

        ServerSocket server = new ServerSocket(port);

        while (true) {
            Socket client = server.accept();

            // HTTPリクエストを出力
            outputRequest(client);

            // HTTPレスポンスを出力
            outputResponse(client, documentRoot);
        }
    }

    private static void outputRequest(Socket client) throws IOException {
        System.out.println("------------------------------------------");

        BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));

        // 1行目からリクエストパスを取得しておく(/でアクセスされた場合はindex.htmlを表示)
        String inline = br.readLine();
        requestPath = inline.split(" ")[1];
        if (requestPath.endsWith("/")) {
            requestPath += "index.html";
        }

        while (br.ready() && inline != null) {
            System.out.println(inline);
            inline = br.readLine();
        }

        System.out.println("------------------------------------------");
    }

    private static void outputResponse(Socket client, String documentRoot) throws IOException {
        PrintStream ps = new PrintStream(client.getOutputStream());

        String responseFile = documentRoot + requestPath;
        File file = new File(responseFile);

        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);

            // ヘッダー出力
            int bodyLength = (int) file.length();
            ps.println("HTTP/1.1 200 OK");
            ps.println("Content-Length: " + bodyLength);
            ps.println("Content-Type: " + getContentType(file));
            ps.println("");

            // ボディ出力
            byte buf[] = new byte[bodyLength];

            fis.read(buf);
            ps.write(buf, 0, bodyLength);
            ps.flush();
        } catch (FileNotFoundException e) {
            // ヘッダー出力
            ps.println("HTTP/1.1 404 Not Found");
            ps.println("");
            ps.println("<h1>指定されたファイルは存在しません。</h1>");
        } finally {
            if (fis != null) {
                fis.close();
            }
        }

        ps.close();
    }

    private static String getContentType(File file) {
        String extension = getExtension(file);

        String contentType = CONTENT_TYPES.get(extension);

        if (contentType == null) {
            return "text/html";
        } else {
            return contentType;
        }
    }

    private static String getExtension(File file) {
        String path = file.getPath();
        int lastDotPosition = path.lastIndexOf(".");

        String extension = null;
        if (lastDotPosition == -1) {
            extension = "";
        } else {
            extension = path.substring(lastDotPosition + 1).toLowerCase();
        }

        return extension;
    }
}

コードはGistにもペタリ。
https://gist.github.com/2260004

Socket の勉強のためにエコーサーバー書いた

前々から、Jarひとつで動くHTTPサーバーを書いてみたいと思っていて(既にたくさんあると思うけど)、それには Socket でがちゃがちゃしないといけないとの事だったので、重い腰を上げて Socket の勉強をしてみた。

とりあえず、リクエストされた文字列をそのまま返すだけのエコーサーバーを書いてみた。
一応、マルチスレッド対応。

Server

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class EchoServer {

    public static void main(String[] args) throws IOException {

        ServerSocket server = new ServerSocket(5000);

        // クライアントからの接続を待ち受ける
        while (true) {
            Socket client = server.accept();

            new Thread(new EchoServer().new EchoThread(client)).start();
        }
    }

    class EchoThread implements Runnable {

        Socket client;

        public EchoThread(Socket client) {
            this.client = client;
        }

        @Override
        public void run() {
            echo(client);
        }

        private void echo(Socket client) {
            System.out.println("処理開始");

            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
            }

            System.out.println("接続中 : " + client.getRemoteSocketAddress());

            try {
                InputStream in = client.getInputStream();
                OutputStream out = client.getOutputStream();

                int recvByte;
                while ((recvByte = in.read()) != -1) {
                    out.write(recvByte);
                }
            } catch (IOException e) {
                System.err.println(e);
            } finally {
                closeQuietly(client);
            }

            System.out.println("処理終了");
        }

        private void closeQuietly(Socket socket) {
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                // ignore
            }
        }
    }
}

Client

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class EchoClient {

    public static void main(String[] args) throws IOException {

        Socket socket = new Socket("localhost", 5000);
        System.out.println("サーバとの接続を確立 : " + socket.getLocalPort());

        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();

        byte[] data = args[0].getBytes();
        out.write(data);
        System.out.println("送信 : " + new String(data));

        int i = 0;
        byte[] output = new byte[data.length];
        int recvByte;
        while ((recvByte = in.read()) != -1) {
            output[i++] = (byte) recvByte;

            if (i >= data.length) {
                break;
            }
        }

        System.out.println("受信 : " + new String(output));

        socket.close();
    }
}

バイトの読み書きを1バイトずつやってるので、あまり効率的ではないんだろうな。

OpenAM(旧OpenSSO)でシングルサインオンをしてみる

やりたいことは、クロスドメイン(CDSSO)でシングルサインオンを実現し、ユーザーを識別(認可)して、アクセスできるURLを制限したい、というもの。
SSO製品は色々あるようですが、一番とっつきやすそうなOpenAMで試す。
SSOエージェントなど、独特のSSO用語は以下のページがとてもわかり易いです。
http://dev.ariel-networks.com/column/tech/opensso/

前提

SSOサーバーとSSOエージェントは別マシン(同じマシンで実行した(もちろんポートを変えて)が上手くいかなかった為)

  • SSOサーバーのホスト(10.29.56.64) : sso.server.com
  • SSOエージェントのホスト(10.29.55.56) : sso.agent.com

SSOはCookieを用いて、SSO Tokenをやり取りするので、localhostなどでアクセスして設定を行なってしまうと、Cookieのドメイン属性がうまく設定できず、動かない可能性がある。
そのため、設定を行う場合は必ずFQDNでアクセスする。
試しにやる場合は、hostsファイルを編集するのが一番楽かと思います。

hosts

10.29.56.64  sso.server.com
10.29.55.56  sso.agent.com

なお、このhosts設定はSSOサーバーのマシン、SSOエージェントのマシン両方に設定しておく必要がある(どちらもそれぞれのホストを見に行く必要があるため)

環境

  • Tomcat 6.0.35(SSOサーバーマシン用)
    TOMCAT_HOME=C:\Program Files\apache-tomcat-6.0.35-server
  • Tomcat 6.0.35(SSOエージェントマシン用)
    TOMCAT_HOME=C:\Program Files\apache-tomcat-6.0.35-agent
  • Windows XP SP3
  • OpenAM(openam_954.war)
  • J2EE Policy Agent(tomcat_v6_agent_3.zip)

今回は、JavaEEサーバーにエージェントを組み込む、「エージェント型」で動かしてみる。
また、OpenSSOを組み込むTomcat(SSOサーバー用)とSSOエージェントを組み込むTomcat(SSOエージェント用)は、必ず別にする必要がある(SSOエージェントをインストールする際に、SSOサーバー用Tomcatは起動していて、SSOエージェント用Tomcatは停止している必要があるため)

手順

  1. OpenAMをインストール
  2. OpenAMにプロファイルを作成
  3. SSOエージェントをインストール
  4. SSO対象のWebアプリにフィルタを設定
  5. OpenAMにポリシーを作成
  6. 確認

1. OpenAMをインストール

SSOサーバーとして機能するOpenAMは、JavaのWebアプリとして配布されているので、配備はWARをデプロイするだけです。
以下から、WAR(openam_954.war)をダウンロードする。
http://www.forgerock.org/openam.html

WARを以下のように配置する(openam_954.war を openam.war にリネームした)

C:\Program Files\apache-tomcat-6.0.35_server\webapps\openam.war

Tomcatを起動したら、以下にアクセスする。
http://sso.server.com:8080/openam

以下の画面が出れば正常にデプロイされている。

「デフォルト設定の作成」を選択する。
パスワードは、以下。

  • amAdmin : password1
  • UrlAccessAgent : password2

インストールが始まる。

正常に完了したら、「ログインに進む」をクリックする。

「amAdmin」ユーザのID、パスワードを入力すればログインできる(IDは大文字・小文字を区別しないようだ)

ログイン成功!

2. OpenAMにプロファイルを作成

引き続きそのままプロファイルを作成する。
プロファイルは~~みたいなものだと思います。
[アクセス制御]タブ → [/ (最上位のレルム)] → [エージェント]タブ → [J2EE]タブ に移動する。

「エージェント」の「新規…」ボタンをクリックして、プロファイルを作成する。

  • 名前 : tomcatagent
  • パスワード : password3
  • 設定 : 集中
  • サーバーURL : http://sso.server.com:8080/openam
  • エージェントURL : http://sso.agent.com:8080/agentapp

名前、パスワード、サーバーURL、エージェントURLは以降でも使うので、覚えておく。

ついでに、クロスドメインの設定もしておく
先ほど作成した、[tomcatagent] → [SSO]タブ → [クロスドメインSSO] に移動し、「有効」のチェックを付け、ページ上部の「保存」ボタンを押下する。

3. SSOエージェントをインストール

(SSOエージェント用マシンで実施)
まず以下から、ZIPをダウンロードし、解凍する。
http://www.forgerock.org/openam.html
場所は以下にした(この場所をエージェントが参照しているようなので、日本語パス等は避けたほうがいいかも)

C:\work\SSO\j2ee_agents

次に、プロファイルで設定したパスワードをパスワードファイルとして保存しておく必要がある。
以下に、「pass」というファイル名でパスワードファイルを作成した(場所はどこでもいいと思う)

C:\Program Files\apache-tomcat-6.0.35-agent\agentpass\pass

そして、このファイルにパスワードを平文で記述する(ここでは「password3」)

これで、準備が整ったので以下を実行する(SSOサーバー用のTomcatを起動しておくこと)

C:\work\SSO\j2ee_agents\tomcat_v6_agent\bin\agentadmin.bat --install

  1. Enter the Tomcat Server Config Directory Path→ C:\Program Files\apache-tomcat-6.0.35-agent\conf
  2. OpenSSO server URL→ http://sso.server.com:8080/openam
  3. Enter the $CATALINA_HOME environment variable→ C:\Program Files\apache-tomcat-6.0.35-agent
  4. Install agent filter in global web.xml ?→ false
  5. Agent URL→ http://sso.agent.com:8080/agentapp
  6. Enter the Agent Profile name→ tomcatagent
  7. Enter the path to the password file→ C:\Program Files\apache-tomcat-6.0.35-agent\agentpass\pass

あと、忘れずに、以下のフォルダにある「agentapp.war」をエージェント用のTomcatに配備(C:\Program Files\apache-tomcat-6.0.35-agent\webapps)しておく。

C:\work\SSO\j2ee_agents\tomcat_v6_agent\etc

4. SSO対象のWebアプリにフィルタを設定

これで、SSOを行う準備が整ったので、SSO対象のWEBアプリへのリクエストがエージェントを経由するように、WEBアプリのweb.xmlにフィルタを作成する。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    id="WebApp_ID" version="2.5">
    <display-name>SampleWeb2</display-name>

    <filter>
        <filter-name>Agent</filter-name>
        <filter-class>com.sun.identity.agents.filter.AmAgentFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>Agent</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>INCLUDE</dispatcher>
        <dispatcher>FORWARD</dispatcher>
        <dispatcher>ERROR</dispatcher>
    </filter-mapping>

    <servlet>
        <servlet-name>Sample1</servlet-name>
        <servlet-class>org.sample.Sample1</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Sample1</servlet-name>
        <url-pattern>/Sample1</url-pattern>
    </servlet-mapping>
    <servlet>
        <servlet-name>Sample2</servlet-name>
        <servlet-class>org.sample.Sample2</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Sample2</servlet-name>
        <url-pattern>/Sample2</url-pattern>
    </servlet-mapping>
</web-app>

大事なところはフィルタ「Agent」です。これでこのアプリへの全てのリクエストがエージェントを経由することになる。

ちなみにサンプル用のWEBアプリは以下のような構成。

アプリケーション アクセスパス アクセスできるユーザー
SampleWeb1 /SampleWeb1/Sample1 sample0, sample1
/SampleWeb1/Sample2 sample0, sample1
SampleWeb2 /SampleWeb2/Sample1 sample0, sample2
/SampleWeb2/Sample2 sample0, sample2

5. OpenAMにポリシーを作成

最後に、OpenAMにどのURLをどのユーザに許可するなどのポリシーを作成する。
まずユーザーを作成しておく。
[アクセス制御]タブ → [/ (最上位のレルム)] → [対象]タブ で「新規…」ボタンを押下し、ユーザーを作成する。
今回は先ほど書いてるように「sample0, sample1, sample2」を作成した。

次にポリシーを作成する。
[アクセス制御]タブ → [/ (最上位のレルム)] → [ポリシー]タブ で「新規ポリシー…」ボタンを押下する。

一般
名前 : SampleWeb1

ルール
サービスタイプ : URL ポリシーエージェント (リソース名あり)
名前 : Sample1
リソース名 : http://sso.agent.com:8080/SampleWeb1/Sample1
アクション : GET,POST(共に許可)

同様にして「/SampleWeb1/Sample2」を許可するルールも追加しておく。

対象
タイプ : OpenAM アイデンティティー対象
名前 : identity1
sample0, sample1 を選択

SampleWeb2用の新規ポリシーも上記と同様にして作成する。

6. 確認

最後に正しく、SSOとアクセス制御が出来ているか確認する。
エージェントが組み込まれたTomcatが起動していない場合は起動する。

http://sso.agent.com:8080/SampleWeb1/Sample1 にアクセス。未ログインなので、ログイン画面が表示される。

「sample1」でログインすると、正常にアプリにアクセスできる。

http://sso.agent.com:8080/SampleWeb2/Sample1 にアクセス。権限が無いので、403エラーになる。

一旦、ログアウト(http://sso.server.com:8080/openam/UI/Logout にアクセス)して、
再度、http://sso.agent.com:8080/SampleWeb1/Sample1 にアクセス。今度は「sample0」でログインする。

「sample0」は別アプリの /SampleWeb1 と /SampleWeb2 にそれぞれアクセスできる。

なぜ Java の配列は共変で、Generics は共変ではないのか

まず、Java の配列がタイプセーフではない話。

public class ExampleArray {

    public static void main(String[] args) {
        String[] strArray = {"test1", "test2"};
        Object[] objArray = strArray; // 配列は共変なので代入可能

        objArray[0] = new Integer(3); // java.lang.ArrayStoreException
    }
}

上記のように、Java の配列は共変という性質を持っているので、Object[] に String[] を代入することができます。つまり、String[] は Object[] のサブクラスである、ということです。
しかし、Generics の場合は、この性質が当てはまりません(Generics は共変ではない)

public class ExampleGenerics {

    public static void main(String[] args) {
        List<String> strList = new ArrayList<String>();
        strList.add("test1");
        strList.add("test2");

        List<Object> objList = new ArrayList<Object>();

        objList = strList; // コンパイルエラー
    }
}

なぜ、配列は共変で、Generics は共変ではないのか?(一緒の方が分かりやすいのに)

その理由の前に、
配列の例で見たように、型の混入現象は、強い型付け言語である Java としては避けたい現象です。
配列の場合は、不正な型が混入した場所で例外(java.lang.ArrayStoreException)を投げてくれます。
(これは、結構大事な所で、使用時(get)ではなく、設定時(set)にきちんと例外を投げてくれると、バグの混入場所が特定しやすいです)
なぜこんなことができるかというと、配列は自分が何の型であるかを自身で知っている(バイトコードに型情報が存在する)ので、違う型を入れたときに、自分と違うということが判定できるのです。

一方、Generics の場合はコンパイル時に型消去という操作が行われます。
型消去については、別エントリーでも書いてます(Generics(Java)の型消去について

Generics のコードは、コンパイル後にはその型情報を一切残していないので、java.lang.ArrayStoreException のような例外を投げることができません。
そのため、共変ではなくして、型の混入を防いでいるのではないかと思います。

Generics で共変っぽいことをしたい場合は、extends とかの境界条件をつければ可能です。

JPA Hibernate (H2 database) をやってみる

最近、Play framework が面白いなぁと思ってちょろちょろ遊んでたりするんですが、Play はモデル層に JPA を採用していて、実は JPA って一度も触った事が無かったのでこれを機にどういうものなのか試して見ました。

プロジェクトの準備

JPA は昔は Java EE(EJB) でしか使えなかったらしいですが、スタンドアロンでも使えるようになったらしいので、スタンドアロンで試してみます。

JPA の実装は Hibernate を使用します。
この Hibernate がやたら依存モジュールが多いので、簡単のため maven プロジェクトにします。

mvn archetype:create -DgroupId=com.example -DartifactId=JPASample

pom.xml に以下を書いておく。
pom.xml に追加する jar はここらへんを参考にしました。注意点としては、JPA を使用するには、hibernate-entitymanager というモジュールが必要なのでそれを必ず追加しておくこと。
あとは、永続化先に使用する DB の JDBC も忘れずに追加しておくこと(今回は H2 database を使用)

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>JPASample2</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>JPASample2</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>3.6.8.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>3.6.8.Final</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.6.4</version>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.3.162</version>
        </dependency>
    </dependencies>
</project>

できたプロジェクトを Eclipse にインポートさせるため、以下のコマンドを叩いて Eclipse プロジェクトにしておく。

cd JPASample
mvn eclipse:eclipse

できたら、Eclipse からインポートする。

データベース の準備

永続先として今回は H2 database を使用。Jar ファイル1つで完結するのでとても簡単。
ここから Jar を DL して Jar 実行。

java -jar h2-1.3.162.jar

Web コンソールが立ち上がると思うので、ログイン(ID、パスワードはそのままでOK)して、テーブルを作成しておく。

CREATE TABLE EMPLOYEE(ID INT PRIMARY KEY, NAME VARCHAR(255));

JPA の準備

まずテーブルにマッピングさせるエンティティクラスを作成する。

@Entity
public class Employee {

    @Id
    public int id;
    public String name;

    public Employee() {
    }

    public Employee(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

次に、JPA が DB の設定などを行う persistence.xml を作成する。
配置場所はクラスパスが通っている場所に配置する(今回は src/main/resources/META-INF/persistence.xml に配置した)
Eclipse でちゃんとクラスパス設定を行うこと。

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
    xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">

    <persistence-unit name="sample" transaction-type="RESOURCE_LOCAL">
        <class>com.example.Employee</class>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
            <property name="hibernate.connection.driver_class" value="org.h2.Driver" />
            <property name="hibernate.connection.username" value="sa" />
            <property name="hibernate.connection.password" value="" />
            <property name="hibernate.connection.url" value="jdbc:h2:tcp://localhost:9092/demo" />
            <property name="hibernate.max_fetch_depth" value="3" />
        </properties>
    </persistence-unit>
</persistence>

実行

あとは、JPA を使って実行するのみ。

public class App {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("sample");
        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            em.persist(new Employee(1, "近藤 勇"));
            em.persist(new Employee(2, "土方 歳三"));

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
            emf.close();
        }
    }
}

H2 database にデータがきちんと登録されていれば成功。

Generics(Java)の型消去について

Java の Generics の実装方式の型消去についてちょっと調べました。

そもそも ジェネリックプログラミングというのは、Java だけにあるものではなく色んな言語に同様の機能があるようです。
ジェネリックプログラミング wikipedia

Java の場合、言語仕様的に、どうやって Generics を実装しているかというと「型消去(type erasure)」によって行われている。
型消去とは、「コンパイル後のバイトコードに型情報を残さない」ということ。

具体的なサンプルコードで見てみる。

public void method15() {
    List<String> list = new ArrayList<String>();
    list.add("string");
    String str = list.get(0);
}

public void method14() {
     List list = new ArrayList();
     list.add("string");
     String str = (String) list.get(0);
}

これを、jad したものが以下。

  public void method15();
     0  new java.util.ArrayList [15]
     3  dup
     4  invokespecial java.util.ArrayList() [17]
     7  astore_1 [list]
     8  aload_1 [list]
     9  ldc <String "string"> [18]
    11  invokeinterface java.util.List.add(java.lang.Object) : boolean [20] [nargs: 2]
    16  pop
    17  aload_1 [list]
    18  iconst_0
    19  invokeinterface java.util.List.get(int) : java.lang.Object [26] [nargs: 2]
    24  checkcast java.lang.String [30]
    27  astore_2 [str]
    28  return

  public void method14();
     0  new java.util.ArrayList [15]
     3  dup
     4  invokespecial java.util.ArrayList() [17]
     7  astore_1 [list]
     8  aload_1 [list]
     9  ldc <String "string"> [18]
    11  invokeinterface java.util.List.add(java.lang.Object) : boolean [20] [nargs: 2]
    16  pop
    17  aload_1 [list]
    18  iconst_0
    19  invokeinterface java.util.List.get(int) : java.lang.Object [26] [nargs: 2]
    24  checkcast java.lang.String [30]
    27  astore_2 [str]
    28  return

確かに、コンパイルされると型情報が消去されて、Java 1.4 時代のものと同じになっている。

なぜ、Java では型消去という実装方式を採用したか?

C++ の場合、同様の機能にテンプレートというものがあるらしく、そちらはコンパイル後も型情報をインライン展開して残すらしいです。
で、Java の場合、なぜ型消去という実装方式を採用したかというと、「後方互換性」のためらしい。

先の例で見たように、コンパイル後のコードが全く同じになるので、1.4 以前で書かれたコードと 1.5 以上で書かれてコードがコードが混在していても実行できる、ということ。

なるほど、よく出来てる。

Servlet 3.0 のアノテーションでは Filter の順番を指定できない?

Servlet 3.0 になってから HttpServlet クラスの @WebServlet 同様に、Filter クラスも @WebFilter というアノテーションが指定できるようになっていて、やたら便利です。

@WebFilter(dispatcherTypes = { DispatcherType.REQUEST }, urlPatterns = { "/*" },
        initParams = { @WebInitParam(name = "encoding", value = "UTF-8") })
public class EncodingFilter implements Filter {

    private String encoding = null;

    @Override
    public void init(FilterConfig fConfig) throws ServletException {
        encoding = fConfig.getInitParameter("encoding");
    }

    @Override
    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        request.setCharacterEncoding(encoding);
        response.setCharacterEncoding(encoding);
        chain.doFilter(request, response);
    }
}

が、どうもアノテーションだけでは、Filter クラスの順番を定義することはできないようです。
なので、結局、web.xml に Filter を上から順番に書いていくしかないようです、残念。
(指定する方法があったら教えて欲しいです)

ちなみに @WebFilter に指定するアノテーション定義はこちら
http://docs.oracle.com/javaee/6/api/javax/servlet/annotation/WebFilter.html

やはり、順番を指定する指定は無いっぽい。

Home

Search
Feeds
Meta

Return to page top