見出し画像

Apache Commons VFS のすすめ (5)

はじめに

今回は、独自のファイルシステムにアクセスする Commons VFS のプロバイダーの作成手順を紹介します。

Commons VFS を使用する場合、デフォルトではサポートしていないシステムにアクセスしたいときには、基本的に他の方が作成したプロバイダーを探して導入することが普通ですので、あまり役立たないかもしれません。

しかし、Commons VFS の内部処理を知ることで、どのように動いているかを理解することは Commons VFS を使う上で助けになるはずです。

Apache Commons VFS のすすめ (1)
Apache Commons VFS のすすめ (2)
Apache Commons VFS のすすめ (3)
Apache Commons VFS のすすめ (4)

作成するもの

例として、今回作成するのは、ローカルファイルの URL のようにアクセスする FTP ファイルシステムを提供します。
このファイルシステムでは、ホスト名は URL で指定するのではなく、オプションで指定して接続します。

また、サンプルコードの中では FTP 接続に Commons Net の FTP クライアントを使用しています。

URL の決定

独自のファイルシステムをサポートする際、ファイルシステムにアクセスするためのスキーマと URL の種類を決める必要があります。

スキーマは、ファイルシステムを識別するための識別子となり、Apache Commons VFS 内で一意となっている必要があるため、既存のものと被らないものにする必要があります。

今回はローカルファイルのような FTP と言うことで "lftp" とします。

スキーマ: lftp

次に使用する URL の種類を決めます。

URL の種類は、2 種類あります。
"http://www.example.com/a/b/c" のようにホスト名が入っている URL と "file:///C:/a/b/c" のようにホスト名が入らない URL です。

今回は、ホスト名をオプションで指定して、URL にはホスト名を含めない形にしますので、後者の URL を使用します。

FileNameParser

URL を決めたら、継承する基本クラスを決めて実装します。

public class LftpFileNameParser extends GenericFileNameParser {
    private static final LftpFileNameParser INSTANCE = new LftpFileNameParser();

    public static LftpFileNameParser getInstance() {
        return INSTANCE;
    }
}

継承する実装クラスは以下の様になります。
今回はローカルファイルのような URL にするため、GenericFileNameParser を継承するようにします。

  • URLFileNameParser
    ホスト名やパス、クエリ文字列などを含んだ通常の URL を解析する。
    例: http://www.example.com/a/b/c

  • GenericFileNameParser
    ホスト名のないパスから始まるローカルファイルのような URL を解析する。
    例: file:///C:/a/b/c

FileSystemConfigBuilder

ファイルシステムにおいて、オプションが必要である時、FileSystemConfigBuilder を継承したクラスを作成して、FileSystemOptions に対する各オプションのアクセサー関数を作成します。

FileSystemOptions に値を設定する関数は FileSystemConfigBuilder に protected 関数として実装されており、FileSystemConfigBuilder を継承したクラスを作成しなければ設定関数を利用することが出来ません。

public class LftpFileSystemConfigBuilder extends FileSystemConfigBuilder {
    private static final LftpFileSystemConfigBuilder BUILDER = new LftpFileSystemConfigBuilder();

    private static final String PREFIX = LftpFileSystemConfigBuilder.class.getName();

    private static final String SYSTEM_TYPE = PREFIX + ".SystemType";

    public static LftpFileSystemConfigBuilder getInstance() {
        return BUILDER;
    }

    @Override
    protected Class<? extends FileSystem> getConfigClass() {
        return LftpFileSystem.class;
    }

    public String getSystemType(FileSystemOptions opts) {
        return getParam(opts, SYSTEM_TYPE);
    }

    String getSystemTypeOrDefault(FileSystemOptions opts) {
        return Optional.ofNullable(getSystemType(opts))
            .orElse(FTPClientConfig.SYST_UNIX);
    }

    public void setSystemType(FileSystemOptions opts, String systemType) {
        setParam(opts, SYSTEM_TYPE, systemType);
    }

    public UserAuthenticator getUserAuthenticator(FileSystemOptions opts) {
        return DefaultFileSystemConfigBuilder.getInstance()
            .getUserAuthenticator(opts);
    }

    public void setUserAuthenticator(FileSystemOptions opts, UserAuthenticator userAuthenticator) {
        DefaultFileSystemConfigBuilder.getInstance()
            .setUserAuthenticator(opts, userAuthenticator);
    }
}

FileProvider

FileProvider が独自ファイルシステムの開始クラスとなります。
このクラスを作成する時には、基本的にほとんどの実装を提供している AbstractOriginatingFileProvider を継承して実装します。

public class LftpFileProvider extends AbstractOriginatingFileProvider {
    @Override
    public Collection<Capability> getCapabilities() {
        :
    }

    @Override
    protected FileSystem doCreateFileSystem(FileName rootFileName, FileSystemOptions fileSystemOptions) throws FileSystemException {
        :
    }
}

AbstractOriginatingFileProvider を継承した場合、2 つの関数を実装する必要があります。

getCapabilities() と doCreateFileSystem() です。
また、場合によってはコンストラクタを実装して、ロジックを追加する必要があります。

登録

作成した FileProvider を Commons VFS に登録するには、"vfs-providers.xml" にエントリを追加して登録する必要があります。

パス: META-INF/vfs-providers.xml

Commons VFS はこのファイルを読み込んで、FileProvider を登録し、ファイルシステムを処理します。

<providers>
	<provider class-name="org.example.vfs.provider.lftp.LftpFileProvider">
		<scheme name="lftp"/>
	</provider>
</providers>

コンストラクタ

FileNameParser を実装した場合、コンストラクタ内で FileProvider に FileNameParser を設定する必要があります。

今回は、GenericFileNameParser を継承したクラスを実装しましたので、コンストラクタ内でインスタンスを登録します。

ただし、AbstractOriginatingFileProvider は親クラスの AbstractFileProvider で既に GenericFileNameParser のインスタンスが設定されていますので、URLFileNameParser を継承したクラスを使用する時だけ、設定するようにしても問題ありません。

public LftpFileProvider() {
    setFileNameParser(LftpFileNameParser.getInstance());
}

getCapabilities()

getCapabilities() は実装するファイルシステムがサポートする機能を Capability 列挙型の一覧で返します。

この関数で返す Capability 一覧は FileSystem でも使用しますので、パッケージプライベートな static 変数に定義するようにした方が良いです。

static final Collection<Capability> CAPABILITIES = List.of(
    Capability.CREATE, Capability.DELETE, Capability.RENAME,
    Capability.GET_TYPE, Capability.LIST_CHILDREN, Capability.READ_CONTENT,
    Capability.WRITE_CONTENT, Capability.APPEND_CONTENT, Capability.RANDOM_ACCESS_READ
);

@Override
public Collection<Capability> getCapabilities() {
    return CAPABILITIES;
}

Capability 列挙体で定義されている機能は数がありますが、サポートする機能を決めるのは重要です。それぞれがどの機能を示しているのかを理解して、正しい一覧を返すようにします。

また、それぞれに機能によって実装するべき関数があります。下記の説明に記載しましたので、参考にしてください。

  • READ_CONTENT
    内容を読むことが出来る。
    - FileContent#getInputStream()
    - AbstractFileObject#doGetInputStream()

  • WRITE_CONTENT
    内容を書くことが出来る。
    - FileContent#getOutputStream()
    - AbstractFileObject#doGetOutputStream()

  • RANDOM_ACCESS_***
    ランダムアクセスして内容を読み書きできることを示す列挙子です。
    - FileContent#getRandomAccessContent()
    - AbstractFileObject#doGetRandomAccessContent()

    • RANDOM_ACCESS_READ
      ランダムアクセスして、内容を読むことが出来る。
      - RandomAccessContent#read()

    • RANDOM_ACCESS_SET_LENGTH
      ランダムアクセスして、ファイルの長さを設定することが出来る。
      - RandomAccessContent#setLength()

    • RANDOM_ACCESS_WRITE
      ランダムアクセスして、内容を書くことが出来る。
      - RandomAccessContent#write()

  • APPEND_CONTENT
    内容を書き込むとき、追加オプションで書き込める。
    - FileContent#getOutputStream(boolean bAppend)
    - AbstractFileObject#doGetOutputStream(boolean bAppend)

  • ATTRIBUTES
    属性を読み書き出来る。
    - FileContent#hasAttribute()
    - FileContent#getAttribute()
    - FileContent#setAttribute()
    - FileContent#removeAttribute()
    - AbstractFileObject#doGetAttributes()
    - AbstractFileObject#doSetAttribute()
    - AbstractFileObject#doRemoveAttribute()

  • LAST_MODIFIED
    最終更新日時をサポートする。
    しかし、内部では後続の GET_LAST_MODIFIED などが使用されており、こちらを使用していないようである。

  • GET_LAST_MODIFIED
    最終更新日時を取得出来る。
    - FileContent#getLastModifiedTime()
    - AbstractFileObject#doGetLastModifiedTime()

  • SET_LAST_MODIFIED_FILE
    ファイルの最終更新日時を設定できる。
    - FileContent#setLastModifiedTime()
    - AbstractFileObject#doSetLastModifiedTime()

  • SET_LAST_MODIFIED_FOLDER
    フォルダーの最終更新日時を設定できる。
    - FileContent#setLastModifiedTime()
    - AbstractFileObject#doSetLastModifiedTime()

  • SIGNING
    内容の署名をサポートしている。
    jar スキーマのファイルシステムで参照しているようだが、具体的に関係する関数が無いようである。

  • CREATE
    ファイルが作成できる。
    - FileObject#createFile()
    - FileObject#createFolder()
    - AbstractFileObject#doCreateFolder()

  • DELETE
    ファイルを削除出来る。
    - FileObject#delete()
    - FileObject#deleteAll()
    - AbstractFileObject#doDelete()

  • RENAME
    ファイルをリネームできる。
    - FileObject#moveTo()
    - AbstractFileObject#doRename()

  • GET_TYPE
    ファイル種別を取得出来る。
    - FileObject#getType()
    - AbstractFileObject#doGetType()

  • LIST_CHILDREN
    ファイルの子供一覧を取得出来る。
    - FileObject#getChildren()
    - AbstractFileObject#doListChildren()
    - AbstractFileObject#doListChildrenResolved()

  • URI
    ファイルの URI がグローバルに一意である URI で提供しているかを示す。

  • FS_ATTRIBUTES
    ファイルシステムの属性を読み書き出来る。
    - FileSystem$getAttribute()
    - FileSystem$setAttribute()

  • JUNCTIONS
    ジャンクションをサポートしている。
    - FileSystem#addJunction()
    - FileSystem#removeJunction()

  • MANIFEST_ATTRIBUTES
    Jar マニュフェストの属性をサポートしている。
    属性は保存される必要は無い。
    - FileContent#hasAttribute()
    - FileContent#getAttribute()
    - FileContent#setAttribute()
    - FileContent#removeAttribute()
    - AbstractFileObject#doGetAttributes()
    - AbstractFileObject#doSetAttribute()
    - AbstractFileObject#doRemoveAttribute()

  • DISPATCHER
    プロバイダーは FileSystem そのものを提供しない。
    プロバイダーは名前を解決して、FileSystemManager に解決した名前でリクエストし直す。

  • COMPRESS
    圧縮を使用したファイルシステムである。

  • VIRTUAL
    Tar や Zip のようなアーカイブのファイルシステムである。

  • DIRECTORY_READ_CONTENT
    FileContent#getInputStream() でフォルダーの中身を読み込める。

doCreateFileSystem()

FileSystem を作成します。

作成されるファイルシステムはサーバへの接続などの接続単位となりますので、接続クライアントがサーバに接続出来てから、FileSystem に渡すようにします。

@Override
protected FileSystem doCreateFileSystem(FileName rootFileName, FileSystemOptions fileSystemOptions) throws FileSystemException {
    var configBuilder = LftpFileSystemConfigBuilder.getInstance();

    var client = new FTPClient();

    var config = new FTPClientConfig(configBuilder.getSystemTypeOrDefault(fileSystemOptions));
    client.configure(config);

    var authData = UserAuthenticatorUtils.authenticate(fileSystemOptions, AUTHENTICATOR_TYPES);
    try {
        var domain = UserAuthenticatorUtils.toString(authData.getData(UserAuthenticationData.DOMAIN));

        client.connect(domain);
        if (!FTPReply.isPositiveCompletion(client.getReplyCode())) {
            throw new FileSystemException("vfs.provider.ftp/connect-rejected.error", domain);
        }

        var userName = UserAuthenticatorUtils.toString(authData.getData(UserAuthenticationData.USERNAME));
        var password = UserAuthenticatorUtils.toString(authData.getData(UserAuthenticationData.PASSWORD));
        if (!client.login(userName, password)) {
            throw new FileSystemException("vfs.provider.ftp/login.error", domain, userName);
        }

        return new LftpFileSystem(rootFileName, client, fileSystemOptions);
    } catch (FileSystemException e) {
        if (client.isConnected()) {
            try {
                client.disconnect();
            } catch (IOException ex) {
                e.addSuppressed(ex);
            }
        }

        throw e;
    } catch (IOException e) {
        if (client.isConnected()) {
            try {
                client.disconnect();
            } catch (IOException ex) {
                e.addSuppressed(ex);
            }
        }

        throw new FileSystemException("vfs.provider.ftp/connect.error", e, rootFileName);
    } finally {
        UserAuthenticatorUtils.cleanup(authData);
    }
}

FileSystem

FileSystem は接続コンテキスト単位で作成されるクラスです。
このクラスは、スキーマと FileSystemOptions でキャッシュされます。

このクラスを作成する時には、基本的に AbstractFileSystem を継承して作成します。

public class LftpFileSystem extends AbstractFileSystem {
    private final FTPClient client;

    protected LftpFileSystem(AbstractFileName rootFileName, FTPClient client, FileSystemOptions fileSystemOptions) {
        super(rootFileName, null, fileSystemOptions);

        this.client = client;
    }
}

createFile(AbstractFileName name)

この関数は実際の FileObject を作成する関数となります。
作成された FileObject は指定された名前でキャッシュされます。
このため、同じ名前で取得された場合には、この作成関数は呼ばれずにキャッシュされたインスタンスが使用されます。

FileObject

FileObject はファイルやフォルダに対応するクラスです。
このクラスを作成する時には、AbstractFileObject を継承して作成します。

public class LftpFileObject extends AbstractFileObject<LftpFileSystem> {
    private FTPFile ftpFile;

    /**
     * @param name the file name - muse be an instance of {@link AbstractFileName}
     * @param fileSystem the file system
     * @throws ClassCastException if {@code name} is not an instance of {@link AbstractFileName}
     */
    protected LftpFileObject(AbstractFileName name, LftpFileSystem fileSystem) {
        super(name, fileSystem);
    }
}

ファイル操作に必要な操作は基本的に AbstractFileObject が提供している protected な関数をオーバーライドすることで実装します。 

以下に操作を実装する際にオーバーライドして実装できる主な関数を上げておきます。

  • FileType doGetType()
    ファイルの種類を返す。

  • long doGetContentSize()
    ファイルのサイズを返す。
    ディレクトリなどの時には、0 を返す。

  • String[] doListChildren()
    子ファイルの名前一覧を返す。

  • InputStream doGetInputStream(int bufferSize)
    ファイルを読み込むためのストリームを返す。

  • OutputStream doGetOutputStream(boolean bAppend)
    ファイルを書き込むためのストリームを返す。

  • void doDelete()
    ファイルやディレクトリを削除する。

  • void doRename(FileObject newFile)
    ファイルをリネームする。

  • void doCreateFolder()
    ディレクトリを作成する。

FileObject は上記のような操作関数を実装することで、ファイル操作をユーザーに提供します。
ほかにも提供できるファイル操作のがありますので、実際に見てみると良いと思います。

FileContent

ファイルコンテンツを表すクラスですが、FileObject を実装する際にAbstractFileObject を継承している場合、このクラスを実装する必要はありません。
必要な操作は、AbstractFileObject を継承したクラスで実装できるようになっています。

まとめ

駆け足となりましたが、自力でファイルシステムを実装する際にどのように実装するのかを説明いたしました。

Commons VFS は普通のファイルシステムのように様々なファイルシステムに共通 API でアクセスできるようになっており、それらがどのように実現されているのかを、独自のファイルシステムを実際に実装してみることで分かることもあると思います。

このように内部実装を知ることで、ライブラリとして使うとき、Commons VFS の使い方について深く理解できるのではないかと思います。

#プログラミング #Java #Apache #エンジニア#ITエンジニア#開発#ウイングアーク#ウイングアーク1st#テックブログ#エンジニア転職#エンジニア採用


この記事が気に入ったらサポートをしてみませんか?