JerseyでJSONシリアライズ対象外のプロパティを指定する (1)

JerseyでJSONを帰す場合には、以下のいずれかの方法を使用します。

対象オブジェクトのうち特定のプロパティをシリアライズ対象外とするには、JAXBを使用する場合には、@XmlTransientをシリアライズ対象外にしたいプロパティに付与します。
JAXBを使用しない場合には、Jacksonというライブラリを使用してJSONにシリアライズされるため、こちらにあるとおりJackosnでの指定が必要になります。このドキュメントを見ると以下のいずれかの方法が使用できます。

  • プロパティに @JsonIgnoreをつける
  • クラスに@JsonIgnorePropertiesをつけて無視するプロパティのリストを記述する
  • JacksonMixInAnnotationsを使用する

最初の2つの方法は対象オブジェクトにアノテーションをつける必要があり、Jacksonへの依存が発生することが問題になる場合には、JacksonMixInAnnotationsを使用することになります。Jerseyから使用するときの設定方法まで調べていないのですが。。
また、JacksonはJAXBのアノテーションも読み取るようなので、結局、JAXBの@XmlTransientでもよいようです。そんなに気にする必要はないと思いますが、JAXBのアノテーションをつけるのがいやでJacksonを使っている場合には本末転倒になりますね。。

IEでのみブラウザ内でPDFファイルを表示できない場合

Content-TypeもContent-Dispositionの設定も問題ないはずなのにIEでのみ、PDFファイルのブラウザ内での表示ができずにダウンロードダイアログが表示されてしまう。
レスポンスヘッダのContent-Typeにいつの間にかcharsetが付加されていることが原因でした。

Content-Type:application/pdf; charset=UTF-8

サーブレットから直接レスポンスを生成した場合には問題ないですが、JSPタグでフォワードして生成しようとしたら付加されていました。回避する方法がわからなかったのでリダイレクトに変更。

HttpServiceとWAB(Web Application Bundle)

GlassfishでWebアプリケーションからOSGiバンドルを利用する
前回のサンプルでは、Webアプリケーション自体はwarとしてディプロイし、依存するサービスをOSGiバンドルとしてディプロイして@Resourceでインジェクションしていました。今回は、アプリケーション自体をOSGiバンドルとしてディプロイする方法として以下を試してみます。

  1. HttpService
  2. Web Application Bundle(WAB)

HttpService

HttpServiceは、OSGi環境でサーブレットを利用するための仕組みです。特定のコンテキストパスにサーブレットや静的リソースを紐付けて登録します。ただし、Servlet2.2までしか対応していないためFilterやJSPが使用できません。Felixの個別実装のようですが、Filterも使えるExtHttpServiceというのもあります。
GlassfishでHttpServiceを使用する場合には、まず、こちらからHttpServiceを実装したバンドルをダウンロードしてディプロイしておきます。
次にBundleActivatorにて、HttpService#registerServletサーブレットを登録します。以下のサンプルでは、/helloにHelloServletを登録し、/imagesに/com/azuki3/osgi/hello/httpservice/imagesパッケージにあるリソースを登録しています。

public class HelloActivator implements BundleActivator {

   public void start(BundleContext context) throws Exception
   {
      ServiceReference sRef = context.getServiceReference(HttpService.class.getName());
      if (sRef != null)
      {
         HttpService service = (HttpService) context.getService(sRef);
         HelloServlet servlet = new HelloServlet();
         service.registerServlet("/hello", servlet, null, null);
         service.registerResources("/images", "/com/azuki3/osgi/hello/httpservice/images", null);
      }
   }

   // ....
}

バンドルを作成してディプロイすると、http://localhost:8080/osgi/helloサーブレットにアクセスできます(最初のosgiというパスはHttpServiceを使用する場合の固定値のようです)。実際には、HttpServiceなど利用するサービスがActiveになった時点でサーブレットを登録し、解除する処理も必要になります。

Web Application Bundle(WAB)

WABは、OSGi 4.2で追加されたエンタープライズ向けの仕様で、warファイルのマニフェストOSGiバンドルの情報とコンテキストパス等を追加することで、Webアプリケーションとして直接、OSGiコンテナにディプロイする方法です。
WABに含まれるサーブレットから、前回のサンプルで作成したHelloServiceを利用してみます。まず、NetBeansMaven Webアプリケーションプロジェクトを作成します。作成したプロジェクトのpom.xmlOSGi関連の記述を追加します。

<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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.azuki3</groupId>
    <artifactId>WABHelloClient</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>WABHelloClient Java EE 6 Webapp</name>
    <dependencies>
        <dependency>
            <groupId>org.osgi</groupId>
            <artifactId>org.osgi.core</artifactId>
            <version>4.2.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>MavenHelloServiceAPI</artifactId>
            <version>${project.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>osgi-cdi-api</artifactId>
            <version>3.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.felix</groupId>
                <artifactId>maven-bundle-plugin</artifactId>
                <version>2.0.1</version>
                <extensions>true</extensions>
                <executions>
                    <execution>
                        <id>bundle-manifest</id>
                        <phase>process-classes</phase>
                        <goals>
                            <goal>manifest</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <manifestLocation>${project.build.directory}/META-INF</manifestLocation>
                    <supportedProjectTypes>
                        <supportedProjectType>bundle</supportedProjectType>
                        <supportedProjectType>war</supportedProjectType>
                    </supportedProjectTypes>
                    <instructions>
                        <Bundle-SymbolicName>${pom.artifactId}</Bundle-SymbolicName>
                        <Web-ContextPath>hello</Web-ContextPath>
                    </instructions>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.0.2</version>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.1-alpha-2</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                    <attachClasses>true</attachClasses>
                    <archive>
                        <manifestFile>${project.build.directory}/META-INF/MANIFEST.MF</manifestFile>
                        <manifestEntries>
                          <Bundle-ClassPath>WEB-INF/classes</Bundle-ClassPath>
                        </manifestEntries>
                    </archive>
		</configuration>
            </plugin>
        </plugins>
        <finalName>WABHelloClient</finalName>
    </build>
</project>

pom.xmlのポイントは以下のとおりです。

  • packagingはwarにする
  • Declarative Serviceをインジェクションするためのアノテーションを含むosgi-cdi-apiをdependencyに追加する
  • maven-bundle-pluginの設定のWeb-ContextPathでコンテキストパスを指定する

Web-ContextPathの値はMANIFEST.MFに反映されディプロイ時に使用されます。
サーブレットは以下のようになります。HelloServiceに@Injectと@OSGiServiceというアノテーションを付加しています。これで別のOSGiバンドルのDSがインジェクションされます。

@WebServlet(name="HelloClient", urlPatterns={"/HelloClient"})
public class HelloClientServlet extends HttpServlet {

    @Inject
    @OSGiService(dynamic=true)
    HelloService helloService;

    /**
     * Processes requests for both HTTP <code>GET</code> and <code>POST</code> methods.
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException if a servlet-specific error occurs
     * @throws IOException if an I/O error occurs
     */
    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {
            out.println("<h1>Servlet HelloClient at " + request.getContextPath() + "</h1>");
            out.println(helloService.sayHello("Duke"));

            out.println("</body>");
            out.println("</html>");
        } finally {
            out.close();
        }
    }

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

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    }
}

バンドルを作成してディプロイすると、http://localhost:8080/hello/HelloClientサーブレットにアクセスできます。

まとめ

HttpServiceは、Javaのコードからサーブレットやリソースを登録していくのがおもしろいですね。提供される機能はかなりシンプルなものですが、ApacheのWeb ConsoleもHttpServiceの上に構築されているようなので、用途によっては使えそうです。例えばクライアントがFull AjaxのWebアプリケーションで、サーバサイドはJAX-RS(Jersey)のみでよいようなケースなど。
一方、WABはwarとほぼ同じイメージで使えるので、Spring MVCなどの現在よく使われるWebフレームワークを使用するのであれば、こっちを使うことになると思います。その上で、アプリケーションに含まれるサービスやコンポーネントを上手く別バンドルに切り出して、バンドル毎に開発していくのが、OSGiを利用した開発のイメージということになるのでしょうか。今後はもう少し実践的なアプリケーションを作ってみたいと思います。

今回のコードは以下にあります。
kenichiro22 / glassfish-osgi-sample / overview – Bitbucket

Javaでtarファイルを操作する

Javaでtarファイルを操作する場合には、Commons Compressを使用するのが簡単そうです。Commons Compresは他にもbzip2やgzipにも対応しています。サイトのサンプルとJavadocを見ればだいたいの使い方はわかると思いますが。

File[] filesToArchive = {file1, file2}
File tarFile = new File("C:\\temp\\foo.tar");
TarArchiveOutputStream out = new TarArchiveOutputStream(new FileOutputStream(tarFile));
for(File f : filesToArchive){
	out.putArchiveEntry(new TarArchiveEntry(f, f.getName()));
	out.write(org.apache.commons.io.FileUtils.readFileToByteArray(f));
	out.closeArchiveEntry();
}
out.close();

tarがファイルのユーザ情報やパーミッションなどのヘッダ情報を保持できるため、TarArchiveEntryのコンストラクタにFileオブジェクトを渡すことで、追加するファイルを元にエントリのヘッダ情報を生成しています*1。ヘッダ情報はバイト列などからも指定可能です。

TarArchiveInputStream in = new TarArchiveInputStream(new FileInputStream(C:\\temp\\foo.tar"));
assertEquals(file1.getName(), in.getNextTarEntry().getName());
assertEquals(file2.getName(), in.getNextTarEntry().getName());
assertNull(in.getNextTarEntry()); // ファイルは2個だけ

*1:http://commons.apache.org/compress/apidocs/index.html?org/apache/commons/compress/archivers/tar/TarArchiveOutputStream.html

GlassfishでWebアプリケーションからOSGiバンドルを利用する

NetBeans IDE の Maven を使用した OSGi 宣言型サービス
このサンプルを動かしてみました。Glassfishは通常のWebアプリケーション(war/ear)と同じようにOSGiバンドルがディプロイできます。このサンプルは、GlassfishにディプロイしたOSGiバンドルが公開する宣言型サービス(DS:Declarative Service)を、別のWebアプリケーションから利用するというものです。宣言型サービスのためのOSGiバンドルとして、

  • サービスAPIバンドル
  • サービスバンドル

の二つのバンドルを作成します。サービスAPIバンドルではインタフェースのみ公開し、サービスバンドルで実装クラスを公開します。WebアプリケーションはOSGiバンドルではなく通常のwar形式でディプロイします。

サービスAPIバンドルの作成

NetBeansでは、Mavenを利用してOSGiバンドル用のプロジェクトが作成できます。新規プロジェクトの作成 -> Maven OSGiバンドルを選択します。

生成されたpom.xmlは、以下のようにpackagingがbundleとなり成果物としてOSGiバンドルが作成できるようになります。

<artifactId>MavenHelloServiceAPI</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>bundle</packaging>
<name>MavenHelloServiceAPI OSGi Bundle</name>

まず、サービスのインタフェースを以下の内容で作成します。

package com.azuki3.mavenhelloserviceapi;

public interface HelloService {
    public String sayHello(String name);
}

そして、プロジェクトのプロパティ->パッケージをエクスポート からパッケージを公開する設定にします。

これによりpom.xmlmaven-bundle-pluginの設定に、Export-Packageの記述が追加されます。

<plugin>
    <groupId>org.apache.felix</groupId>
    <artifactId>maven-bundle-plugin</artifactId>
    <version>2.0.1</version>
    <extensions>true</extensions>
    <configuration>
        <instructions>
            <Export-Package>com.azuki3.mavenhelloserviceapi</Export-Package>
            <Private-Package>com.azuki3.mavenhelloserviceapi.*</Private-Package>
        </instructions>
    </configuration>
</plugin>

作成したバンドルのMANIFEST.MFは以下のようになります。

Manifest-Version: 1.0
Export-Package: com.azuki3.mavenhelloserviceapi
Build-Jdk: 1.6.0_21
Bundle-Version: 1.0.0.SNAPSHOT
Tool: Bnd-0.0.357
Bundle-Name: MavenHelloServiceAPI OSGi Bundle
Bnd-LastModified: 1307453703590
Created-By: Apache Maven Bundle Plugin
Bundle-ManifestVersion: 2
Bundle-SymbolicName: com.azuki3.MavenHelloServiceAPI
Import-Package: com.azuki3.mavenhelloserviceapi

サービスバンドルの作成

このインタフェースを実装するサービスのためのプロジェクトを同様に作成します。そしてプロジェクトの依存リソースとして、

  • 先ほど作成したAPIバンドル
  • org.apache.felix.scr.annotations

を追加します。org.apache.felix.scr.annotationsは、DSを定義するためのXMLファイルを自動生成するツール用のアノテーションです。実際のクラスは以下のようになります。@Componentと@ServiceがDSのためのアノテーションです。

package com.azuki3.mavenhelloservice.impl;

import com.azuki3.mavenhelloserviceapi.HelloService;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Service;

@Component(name="hello-service")
@Service
public class HelloImpl implements HelloService {
    @Override
    public String sayHello(String name) {
        return "Hello " + name;
    }
}

次にpom.xmlに、このアノテーションを処理するためのscrプラグインを追加します。

<plugin>
    <groupId>org.apache.felix</groupId>
    <artifactId>maven-scr-plugin</artifactId>
    <executions>
        <execution>
            <id>generate-scr-scrdescriptor</id>
            <goals>
                <goal>scr</goal>
            </goals>
        </execution>
    </executions>
</plugin>

この状態でビルドするとOSGI-INFフォルダにDSの定義が記述されたserviceComponents.xmlが作成されます。

<?xml version="1.0" encoding="UTF-8"?>
<components xmlns:scr="http://www.osgi.org/xmlns/scr/v1.0.0">
    <scr:component enabled="true" name="hello-service">
        <implementation class="com.azuki3.mavenhelloservice.impl.HelloImpl"/>
        <service servicefactory="false">
            <provide interface="com.azuki3.mavenhelloserviceapi.HelloService"/>
        </service>
        <property name="service.pid" value="hello-service"/>
    </scr:component>
</components>

OSGiバンドルを使用するWebアプリケーションの作成

NetBeansMaven Webアプリケーションを選択して新規プロジェクトを作成します。プロジェクトの依存リソースにサービスAPIバンドを追加しscopeをprovidedに変更します(OSGiバンドルとして利用するため)。

<dependency>
      <groupId>${project.groupId}</groupId>
      <artifactId>MavenHelloServiceAPI</artifactId>
      <version>${project.version}</version>
      <scope>provided</scope>
</dependency>

次に、HelloServiceを利用するサーブレットを作成します。@Resourceアノテーションで、DSを定義する際にname属性で指定した値をmappedNameで指定します。

package com.azuki3.mavenhellowebclient;

import com.azuki3.mavenhelloserviceapi.HelloService;
import java.io.IOException;
import java.io.PrintWriter;
import javax.annotation.Resource;
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(name="HelloClient", urlPatterns={"/HelloClient"})
public class HelloClient extends HttpServlet {

    @Resource(mappedName = "hello-service")
    HelloService helloService;

    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {
            out.println("<h1>Servlet HelloClient at " + request.getContextPath() + "</h1>");
            out.println(helloService.sayHello("Duke"));
            out.println("</body>");
            out.println("</html>");
        } finally {
            out.close();
        }
    }

    // ...以下省略
}

OSGiバンドルのディプロイ

GlassfishへのOSGiバンドルのディプロイは以下のいずれかの方法で行います。

  • asadminコマンド
  • autodeployフォルダへのjarのコピー

今回はautodeployフォルダにコピーします。NetBeansからGlassfishを起動している場合、autodeployフォルダは以下の場所になります。ここに各バンドルのプロジェクトのtargetフォルダに作成されたjarファイルをコピーします。

C:\Users\ユーザー名\.netbeans\6.9\config\GF3\domain1\autodeploy\bundles

felixの管理コンソール*1から確認すると、以下のようにバンドルがActiveになっていることがわかります。また、NetBeansGlassfishのコンソールにもログが出力されます。

-> find Hello
START LEVEL 1
   ID   State         Level  Name
[ 286] [Active     ] [    1] MavenHelloService OSGi Bundle (1.1.0.SNAPSHOT)
[ 287] [Active     ] [    1] MavenHelloServiceAPI OSGi Bundle (1.0.0.SNAPSHOT)

warを作成してGlassfishにディプロイし、Webアプリケーションにアクセスします。

http://localhost:8080/HelloClient/HelloClient

"hello duke"と表示されたら、正しくDSが呼び出されています。

まとめ

通常のWebアプリケーションから、OSGiの宣言型サービスを簡単に利用することができました。これにより、依存性解決をある程度までOSGiにゆだねることができることになり、いままでのjarによる依存性解決の問題が軽減されます。ただし、依然としてWebアプリケーション自体はOSGiベースではないので以下のような制約もあるようです。

  • アプリケーション起動時に依存ライブラリが利用可能になっている必要がある
  • 動的にバンドル(DS)を差し替えることができない

これは、今回の例でのHelloClientへのHelloServiceのインジェクションについての制約で、OSGi上ではうまくやってくれると思います。
次回は、WebアプリケーションでのOSGiのその他の使い方を試してみたいと思います。

今回のソースは以下にあります。
kenichiro22 / glassfish-osgi-sample / source – Bitbucket

*1:telnet localhost 6666で接続可能

Glassfish 3.1でautodeployフォルダに入れたjarがディプロイされない

GlassFish Server Open Source Edition 3.1 (build 43)時点での情報で、将来的には変更されるかもしれませんが、ドメインのautodeploy/bundleフォルダにコピーしたjarの自動ディプロイ(インストール)が初期状態で無効になっているようです。
Glassfish Project: users@glassfish.java.net: Archive ― Java.net
このMLのスレッドにあるとおり、いずれかの方法でJVMオプションを変更することで有効にすることができます。私が試したのは、domain.xmlを変更する方法です。

<jvm-options>-Dorg.glassfish.additionalOSGiBundlesToStart=org.apache.felix.shell,org.apache.felix.gogo.runtime,org.apache.felix.gogo.shell,org.apache.felix.gogo.command</jvm-options>

↑を↓に変更する。

<jvm-options>-Dorg.glassfish.additionalOSGiBundlesToStart=org.apache.felix.shell,org.apache.felix.gogo.runtime,org.apache.felix.gogo.shell,org.apache.felix.gogo.command,org.apache.felix.shell.remote,org.apache.felix.fileinstall</jvm-options>

org.apache.felix.fileinstallというバンドルが自動ディプロイ機能を提供しており、これがGlassfish起動時に開始されないことが原因のようですね。
おそらく同じ原因でmodules/autostartにあるjarも自動ディプロイされないため、宣言型サービスなどの機能が使えなくなっていました。Glassfish自体詳しくないので、インストールやドメインの作成方法がまずかった可能性もありますが。

IAMでS3のbucketにアクセスできるユーザーを制限する

AWS ConsoleでAWS Identity and Access Management (IAM)が操作できるようになったので試してみました。しかし、APIを直接叩かなくなくてよくなったから簡単、ではなかったです。
いずれの方法にしろJSON形式のPolicyを定義する必要があります。AWS Policy Generatorもあるのですが、S3のAPIを使ったことがないと対象のリソースと操作(Action)をどう組み合わせると、どうなるかイメージしづらいです。

IAMでユーザーかグループを作成し、それに対してパーミッションを追加します。まずは、特定のbucketにアクセスできるパーミッションとして以下のようなPolicyを作りました。は実際には対象のbucket名です。このPolicyが正しいかは自信ありません...

{
  "Statement": [
    { // (1)
      "Action": "s3:ListAllMyBuckets",
      "Effect": "Allow",
      "Resource": "*"
    },
    { // (2)
      "Action": [
        "s3:ListBuckets"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::<bucket_name>"
    },
    { // (3)
      "Action": "s3:*",
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::<bucket_name>/*"
    }
  ]
}

基本はすべてDenyです。そこに必要な権限を付加していきます。
(1)で自分がアクセスできるBucketの一覧を取得できます。これは、bucket内での操作には直接必要ではないですが、AWS ConsoleやクライアントアプリケーションでBucketの一覧を取得するのに必要なようです。
(2)(3)でbucketの操作権限を付加しています。(2)のActionで"s3:*"とするだけでも良いように思えるのですが、、(3)のようにkey_nameとして*を指定しないとファイルの追加ができませんでした。(3)のActionはもう少し厳しくしたほうがよいと思います(PutObject/GetObject/DeleteObjectのみでもOKかも)。