Content-Disposition によるダウンロードファイル名の指定について

Content-Disposition ヘッダーによるダウンロード時のファイル名の指定について、色々調べたことをまとめます。

まずファイル名の指定方法には2種類あり、ブラウザによって対応状況が違います。

- attachment; filename="hogehoge.pdf"
- attachment; filename*=uti-8'ja'hogehoge.pdf

いずれも RFC 5987 に規定されている書き方に準拠しているので、両方とも正しいです。

IE の場合、IE9 は両方に対応していますが、IE6-8 は前者のみに対応しています。
Chrome (18.0) と Firefox (11.0) は両方に対応しています。Safari (5.1.5) は前者のみで、2バイト文字の取り扱いはできません。

ファイル名はパーセントエンコードする必要があります。パーセントエンコーディングについては、RFC 3986 で規定されています。HTMLのフォームからGET,POST した時にお目にかかるx-www-form-encoded エンコード方式に類似しているものですが、半角スペースを "%20" に変化するか "+" に変換するかの違い等があるので注意が必要です。

- 全ブラウザ
- 半角英数字はパーセントエンコーディングが不要
- 半角スペースは"%20"に。"+" への変換ではNG。
- 2バイト文字はパーセントエンコーディングが必要。
- 半角記号については、ブラウザによって差異がある。
- IE6-8, IE9 (attachment; filename="hogehoge.pdf"の場合)
-- "#" ";" "%" はパーセントエンコーディングが必要。
-- その他の半角記号はエンコーディング不要。
- Chrome (attachment; filename*=uti-8'ja'hogehoge.pdf; の場合)
-- "'" "," ";" "%" はパーセントエンコーディングが必要。
-- その他の半角記号はエンコーディング不要。
-- "(" ")" などデコードができないので、エンコーディングしてはいけない文字もある。
- Firefox (attachment; filename*=uti-8'ja'hogehoge.pdf の場合)
-- Chrome より賢いので、Chrome と同じで良い。

IE系とChromeおよび Firefox の2つで処理を分ければ良いです。

Cassandra へ Java でアクセスする

どうも 6月2日に新しい安定バージョン 0.8.0 がリリースされていた模様。
それだとまた元のプログラムのままでは動かない!

以下が修正後のサンプルです。

  • TBinaryProtocolのパッケージ
  • org.apache.cassandra.thrift.Columnのコンストラクタの引数

が変わっています。

import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.List;

import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnOrSuperColumn;
import org.apache.cassandra.thrift.ColumnParent;
import org.apache.cassandra.thrift.ColumnPath;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.cassandra.thrift.InvalidRequestException;
import org.apache.cassandra.thrift.KeyRange;
import org.apache.cassandra.thrift.KeySlice;
import org.apache.cassandra.thrift.NotFoundException;
import org.apache.cassandra.thrift.SlicePredicate;
import org.apache.cassandra.thrift.SliceRange;
import org.apache.cassandra.thrift.TimedOutException;
import org.apache.cassandra.thrift.UnavailableException;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;

/**
 * @author hatanaka
 */
public class CassandraTest {
    /** キースペース */
    public static final String KEYSPACE = "Keyspace1";

    /** カラムファミリー */
    public static final String COLUMN_FAMILY = "Standard2";

    /**
     * @param args
     */
    public static void main(String[] args) {
        TSocket socket = new TSocket("192.168.1.87", 9160);
        TTransport transport = new TFramedTransport(socket);
        TProtocol protocol = new TBinaryProtocol(transport);
        Cassandra.Client client = new Cassandra.Client(protocol);
        try {
            transport.open();
            client.set_keyspace(KEYSPACE);
            final long timestamp = System.currentTimeMillis();
            //特定キーに1件カラムをインサート
            Column value = new Column(strToBB("sample1"));
            value.setValue(strToBB("サンプルの値"));
            value.setTimestamp(timestamp);
            client.insert(strToBB("test"),
                          new ColumnParent(COLUMN_FAMILY),
                          value,
                          ConsistencyLevel.ONE);
            System.out.println("インサート完了.");

            //特定キーの特定カラムの値を取得
            System.out.println();
            System.out.println("特定キーの特定カラムの値を取得");
            ColumnPath path = new ColumnPath(COLUMN_FAMILY);
            path.setColumn(strToBB("sample1"));
            ColumnOrSuperColumn column = client.get(strToBB("test"),
                                                    path,
                                                    ConsistencyLevel.ONE);
            System.out.println(byteToStr(column.getColumn().getName()) + ":"
                    + byteToStr(column.getColumn().getValue()));

            //特定キーの全カラムの値を取得
            System.out.println();
            System.out.println("特定キーの全カラムの値を取得");
            SlicePredicate predicate = new SlicePredicate();
            predicate.setSlice_range(new SliceRange(ByteBuffer.wrap(new byte[0]),
                                                    ByteBuffer.wrap(new byte[0]),
                                                    false,
                                                    10));
            List<ColumnOrSuperColumn> columns = client.get_slice(strToBB("test"),
                                                                 new ColumnParent(COLUMN_FAMILY),
                                                                 predicate,
                                                                 ConsistencyLevel.ONE);
            for (ColumnOrSuperColumn aColumn : columns) {
                System.out.println(byteToStr(aColumn.getColumn().getName())
                        + ":" + byteToStr(aColumn.getColumn().getValue()));
            }

            //複数キーの全カラムの値を取得
            System.out.println();
            System.out.println("複数キーの全カラムの値を取得");
            KeyRange range = new KeyRange();
            range.setStart_key(new byte[0]);
            range.setEnd_key(new byte[0]);
            List<KeySlice> keys = client.get_range_slices(new ColumnParent(COLUMN_FAMILY),
                                                          predicate,
                                                          range,
                                                          ConsistencyLevel.ONE);
            for (KeySlice key : keys) {
                for (ColumnOrSuperColumn aColumn : key.getColumns()) {
                    System.out.println(byteToStr(key.getKey()) + ":"
                            + byteToStr(aColumn.getColumn().getName()) + ":"
                            + byteToStr(aColumn.getColumn().getValue()));
                }
            }
        } catch (InvalidRequestException e) {
            throw new RuntimeException(e);
        } catch (UnavailableException e) {
            throw new RuntimeException(e);
        } catch (TimedOutException e) {
            throw new RuntimeException(e);
        } catch (TException e) {
            throw new RuntimeException(e);
        } catch (NotFoundException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                transport.flush();
            } catch (TTransportException e) {
                throw new RuntimeException(e);
            } finally {
                transport.close();
            }
        }
    }

    /**
     * String から ByteBuffer へのコンバータ
     * @param msg String
     * @return ByteBuffer
     */
    private static ByteBuffer strToBB(String msg) {
        Charset charset = Charset.forName("UTF-8");
        return ByteBuffer.wrap(msg.getBytes(charset));
    }

    /**
     * byte[] から String へのコンバータ
     * @param buf byte[]
     * @return String
     */
    private static String byteToStr(byte[] buf) {
        Charset charset = Charset.forName("UTF-8");
        return new String(buf, charset);
    }
}

Cassandra に Java でアクセス

Cassandra に Java でアクセスする方法について、試行錯誤した内容をまとめます。
アクセスには高レベルAPIと低レベルAPIの2種類方法があるのですが、ここでは低レベルAPIについて記載しています。

Cassandra の最新版(2011年6月3日時点で0.7.6-2)の場合、公式ページにあるサンプルでは、コンパイルが通りません。おそらく API が変わったのだと思われます。

サンプルと同じ内容のプログラムを、0.7.6-2でコンパイルおよび実行可能なように書き直したものが以下です。

import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.List;

import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnOrSuperColumn;
import org.apache.cassandra.thrift.ColumnParent;
import org.apache.cassandra.thrift.ColumnPath;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.cassandra.thrift.InvalidRequestException;
import org.apache.cassandra.thrift.NotFoundException;
import org.apache.cassandra.thrift.SlicePredicate;
import org.apache.cassandra.thrift.SliceRange;
import org.apache.cassandra.thrift.TBinaryProtocol;
import org.apache.cassandra.thrift.TimedOutException;
import org.apache.cassandra.thrift.UnavailableException;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;

/**
 * @author hatanaka
 */
public class CassandraTest {
    /** キースペース */
    public static final String KEYSPACE = "Keyspace1";

    /** カラムファミリー */
    public static final String COLUMN_FAMILY = "Standard2";

    /**
     * @param args
     */
    public static void main(String[] args) {
        TSocket socket = new TSocket("192.168.28.129", 9160);
        TTransport transport = new TFramedTransport(socket);
        //TSocketを直接引数に取れるが、TFramedTransportでないと、以降の処理でエラー
        TProtocol protocol = new TBinaryProtocol(transport);
        Cassandra.Client client = new Cassandra.Client(protocol);
        try {
            transport.open();
            client.set_keyspace(KEYSPACE);
            final long timestamp = System.currentTimeMillis();
            //特定キーに1件カラムをインサート
            client.insert(strToBB("test"),
                          new ColumnParent(COLUMN_FAMILY),
                          new Column(strToBB("sample1"),
                                     strToBB("サンプルの値"),
                                     timestamp),
                          ConsistencyLevel.ONE);
            System.out.println("インサート完了.");

            //特定キーの特定カラムの値を取得
            ColumnPath path = new ColumnPath(COLUMN_FAMILY);
            path.setColumn(strToBB("sample1"));
            ColumnOrSuperColumn column = client.get(strToBB("test"),
                                                    path,
                                                    ConsistencyLevel.ONE);
            System.out.println(byteToStr(column.getColumn().getName()) + ":"
                    + byteToStr(column.getColumn().getValue()));

            //特定キーの全カラムの値を取得
            SlicePredicate predicate = new SlicePredicate();
            predicate.setSlice_range(new SliceRange(ByteBuffer.wrap(new byte[0]),
                                                    ByteBuffer.wrap(new byte[0]),
                                                    false,
                                                    10));
            List<ColumnOrSuperColumn> columns = client.get_slice(strToBB("test"),
                                                                 new ColumnParent(COLUMN_FAMILY),
                                                                 predicate,
                                                                 ConsistencyLevel.ONE);
            for (ColumnOrSuperColumn aColumn : columns) {
                System.out.println(byteToStr(aColumn.getColumn().getName())
                        + ":" + byteToStr(aColumn.getColumn().getValue()));
            }
        } catch (InvalidRequestException e) {
            throw new RuntimeException(e);
        } catch (UnavailableException e) {
            throw new RuntimeException(e);
        } catch (TimedOutException e) {
            throw new RuntimeException(e);
        } catch (TException e) {
            throw new RuntimeException(e);
        } catch (NotFoundException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                transport.flush();
            } catch (TTransportException e) {
                throw new RuntimeException(e);
            } finally {
                transport.close();
            }
        }
    }

    /**
     * String から ByteBuffer へのコンバータ
     * @param msg String
     * @return ByteBuffer
     */
    private static ByteBuffer strToBB(String msg) {
        Charset charset = Charset.forName("UTF-8");
        return ByteBuffer.wrap(msg.getBytes(charset));
    }

    /**
     * byte[] から String へのコンバータ
     * @param buf byte[]
     * @return String
     */
    private static String byteToStr(byte[] buf) {
        Charset charset = Charset.forName("UTF-8");
        return new String(buf, charset);
    }
}

挿入検索のメソッドの引数が違っているのと、キーストアの指定の仕方も違っています。
簡単なプログラムなのに、perlのサンプルを見たり、色々苦労しました...

マルチインスタンスタスク(続き)

まずはじめに、前回の内容に誤りがあったので訂正します。
マルチインスタンスタスクにおいて、「いくつのインスタンスが生成されるか」という指定は、"loopCardinality" 属性または "loopDataInput" 属性で指定されます。

  • "loopCardinality"属性が使われる場合は、この属性で指定されている値(計算式の結果)に基づいて、インスタンスが生成されます。5の場合は、5つインスタンスが生成されます。
  • "loopDataInput"属性が使われる場合は、この属性で指定された入力データ(コレクションデータであることが前提)の要素の数だけ、インスタンスが生成されます。

いずれの場合においても、評価が行われるのは最初の1度だけで、その時に生成されるインスタンス数が決定されます。


続いて「いつインスタンスが終了するか」に言及します。どこかのタイミングでマルチインスタンスタスク全体を終了させて、トークンを次のノードに向けて動かす必要があります。それはどのタイミングでしょうか。
これは "completionCondition" 属性によって決定されます。マルチインスタンスタスク内のインスタンス1つ1つが終了する度ごとに、この属性の値が評価されます。
評価された結果、true になるとマルチインスタンスタスクは終了します。この時点で終了していないマルチインスタンスタスク内のインスタンスについては、キャンセルされます。
これによって「最初の1つが終了したら、他はすべてキャンセルする」「全部が終了するまで、全体も終了しない」といった終了条件や、もっと複雑な終了条件を表現することができるようになっています。


実は Questetra BPM Suite の「チームタスク」は、この「マルチインスタンスタスク」の特殊な形といえます。
内部実装としてインスタンスは1つしか生成されていないのですが、

  • loopCardinality は 担当者の数
  • completionCondition は 1つインスタンスが終了したら、全体も終了

という形になっているといえます。

マルチインスタンスタスク

BPMN のタスクの種類として、マルチインスタンスタスクというものがあります。プロセスにおいて、トークンがこのマルチインスタンスタスクに到達すると、タスク(インスタンス)が複数生成されます。

たとえば「確認」というタスクがあって、これを複数の人が実施する場合などに使用します。分岐を使ってフローを複数に分けることで表現することもできますが、複数のタスク(インスタンス)でフローを分ける必要がない場合には、シンプルに記述することができます。

ただしこのタスク(インスタンス)の生成の仕方で、大きく2つに分かれます。
「Parallel(並列)」と「Sequential(直列)」の2種類です。マーカーも異なります。

こちらは並列です。トークンが到達すると同時に、複数のインスタンスが生成され実行されます。

こちらは直列です。トークンが到達すると1つインスタンスが生成されます。1つが終わるとその次が生成され、という形で、順々に実行されます。ループタスクというのが別にあるのですが、それとの違いは正直、まだよく解っていないです。後日の課題にします。


さて並列においても直列においても、「いくつのインスタンスが生成されるか」という問題があります。これについては "loopCounter""loopCardinality" または "loopDataInput" 属性で指定します。この属性が "5" の場合、5つインスタンスが生成されます。

もう1つ「いつインスタンスが終了するか」という問題があります。これについては、後日まとめます。

XPDL のデータ定義について(まだまだメモ書き)

XPDLにおいて、ワークフローシステムやBPMS(ビジネスプロセスマネジメントシステム)で使用するデータは "Relevant data field/Property" として定義します。

定義できる個所は複数あるのですが、もっとも解りやすいのはの子要素として定義する形です。
該当するワークフロー/プロセスで使用するデータを定義します。以下は定義のサンプルです。

<DataFields>
	<DataField Id="1" Name="orderNumber" IsArray="FALSE">
		<DataType>
			<BasicType Type="INTEGER" />
		</DataType>
		<Length>0</Length>
		<Description />
	</DataField>
	<DataField Id="3" Name="status" IsArray="FALSE">
		<DataType>
			<BasicType Type="STRING" />
		</DataType>
		<Length>0</Length>
		<Description />
	</DataField>
	<DataField Id="4" Name="orderInfo" IsArray="FALSE">
		<DataType>
			<BasicType Type="STRING" />
		</DataType>
		<Length>0</Length>
		<Description />
	</DataField>
</DataFields>

ID・名前や型の定義ができるのですが、詳しくはまたの機会に。
先にこのデータ定義とタスクの関係について調べます。