Web Performer V2.4の新機能「アクション拡張」を使って複数ファイルの一括ダウンロードを実現してみる

2020-07-15

はじめに

こんにちは。エヌデーデー関口です。
当社ではキヤノンITソリューションズから販売されているWebアプリケーション超高速開発ツールである「Web Performer」を使った開発を行っています。
この記事では2020年5月にリリースされた Web Performer V2.4 の新機能である「アクション拡張」について、実装例を踏まえてを紹介したいと思います。

想定される読者

  • Web Performerは導入済
  • Web Performer V2.4 での開発は未経験
  • Web Performerの標準アクションの機能に課題を抱えている
  • Java プログラム経験者

アクション拡張とは

「アクション拡張」とは、Web Performer のアクション(リンクやボタン)をクリックした時に、通常では画面遷移へのパラメータ送信や、ビジネスプロセスの呼び出しを行うところを、独自の Java で実装したプログラムを呼び出して実行するというものです。

標準機能以外の外部プログラムの呼び出しであれば、「拡張ビジネスプロセス」として、Java プログラムを、他のビジネスプロセスから呼び出すことや、アクション実行時に独自の JavaScript を呼び出すという事も出来ます。では、今回のアクション拡張ではどういった点が異なるのでしょうか。以下の表に主な特徴を簡単にまとめてみました。

拡張ビジネスプロセスアクション時の
JavaScript 呼び出し
アクション拡張
呼び出し元ビジネスプロセスアクションアクション
実装JavaJavaScriptJava
UIの制御××
画面遷移の制御××
画面と経由での別アプリとの連携×
サーバー経由での別アプリとの連携×
データベースアクセス×

このように、アクション拡張ではサーバー側の処理を行いつつ、画面遷移の制御や画面経由(リダイレクト)で他のアプリケーションへ連携するなどの機能が利用出来ます。

今回の実装例について

例えば、クラウドストレージの Box や、OneDrive などの機能にも、ファイルを複数選択してダウンロードという機能があるように、Web Performer で作成した画面で、複数ファイルを選択して、ダウンロードしたいというような要望はないでしょうか。
今回はそのような要望に応えるために、V2.4の新機能である「アクション拡張」で、 複数のファイルを選択して、zipファイルとしてダウンロードするアプリを実装してみます。

Web Performerのアプリケーションを作成する

それでは、早速 Web Performer のアプリケーションを作成してみましょう。

プロジェクトとアプリケーションを作成する

まずは適当に、プロジェクトを sample 、アプリケーションを ACTION_DEMO としておきます。

データモデルを定義する

次にデータベースに見積書のファイルを登録しておき、ダウンロードする度に発行回数という項目をカウントアップできるように、次に示すような FILES という名称のデータモデルを定義します。

項目コード名前キーグループ桁数小数桁データタイプ
IDID120NUM
FILE_NAMEファイル名0500TEXT
LAST_DOWNLOAD_TIME最終ダウンロード時間000TIME
DOWNLOAD_COUNTダウンロード回数030NUM
F_PDFファイル000FILE

そして、データモデルの操作は更新処理のみをこのように定義しておきます。

操作コード名前操作タイプ対象条件独立コミット
UPDATE1時間と回数更新UPDATEID=_IN_.IDなし

こちらの操作コードの操作ロジックは次の通りです。
最終ダウンロード時間を現在時刻で、ダウンロード回数を繰り上げする更新処理です。

項目コード加工式
LAST_DOWNLOAD_TIME@SYSNOW
DOWNLOAD_COUNT_IN_._ITEM_ + 1

なお、Web Performer のデータモデルでFILE型を定義した場合には、データベースのテーブル上に、次の項目が必要になりますので、ご注意ください。

保持情報カラム名実装内容
ファイル名ファイル項目コード_FNM_最大桁数3000の可変長文字列
ファイル・サイズファイル項目コード_FSZ_桁数15の数値
ファイル・タイプファイル項目コード_FTY_最大桁数3000の可変長文字列
ファイル実体ファイル項目コード_FBD_Binary Large OBject

これらの項目は、ツールバーの「スキーマ操作(F7)」の操作で、データモデルからテーブルを作成する場合については、テーブル作成用の DDL に自動的に付加されます。

データモデルとテーブルを作成したら、ダウンロードしたい PDF を登録しておきましょう。
登録方法は、各種データベースの管理ツールから登録してもいいですし、Web Performer でファイル登録用の画面を作ってしまうというのもいいでしょう。今回は登録の仕方については説明を省きます。

ビジネスプロセスの定義

データモデルの次は、画面(入出力)からボタンを押された際に、データベースのテーブル内容を更新するためのロジックをビジネスプロセス BP_ACTION を作成して定義します。以下にロジックを示します。

制御コードデータモデルコード機能コードパラメータ作業コード
INFILES(ファイル情報)IN_FILES
FOREACHIN_FILES
 IFIN_FILES._RECORD_SELECETD=@TRUE
  CALLFILES(ファイル情報)UPDATE1(時間と回数更新)REC
 END
END

拡張定義ファイルの編集

さて、入出力を作成する前に、拡張定義ファイルに、アクション拡張をアプリケーション内で利用するための名称と、拡張のタイプを定義しましょう。

プロジェクトを右クリックして、「新規」>「新規拡張定義ファイル」と選択してください。ファイル名を決定するダイアログが表示されるので、EXT としておきましょう。

プロジェクトの ext フォルダに EXT.wprx というファイルが作成されるので、ダブルクリックして編集を開始します。次のように設定してください。

コード名前タイプ
EXT_MULTI_DL複数ダウンロードIOACTSV 入出力アクション拡張

また、 EXT_MULTI_DL の拡張プロパティには次のように設定してください。

コード名前
impl 実装コードext.MultiDownloadAction
noTransition 画面遷移しないTRUE

impl には実際に動作する Java のクラス名を書きます。noTransition はこのアクションを呼び出した後で、画面遷移をしない場合に TRUE を設定します。今回はダウンロードをするので、画面遷移をしません。

入出力を定義する

次にアクション拡張を使用する入出力(画面)を ACTION_IO(ファイルダウンロード拡張)という名称で作成します。
先ほどの FILES というデータモデルを一覧形式で表示できるようにしておきます。検索などの機能は省略しています。定義の抜粋は次のようになります。

また、プレビューとしては次のようになります。

ダウンロードボタンの定義

ダウンロードを押したときに、ビジネスプロセスを呼び出して、データベースの項目を更新するように加工式に先ほど定義したBP_ACTIONを定義します。

さらに、アクション拡張を呼び出すために、ダウンロードボタンに定義を追加していきます。
入出力アクション(DOWNLOAD)の入出力項目プロパティに、次のような設定を入力します。
先ほどの拡張定義ファイルに定義した、拡張機能のコード( EXT_MULTI_DL )を定義します。

キー
ext 拡張EXT_MULTI_DL

アクション拡張を実装する

いよいよ、本題のアクション拡張の実装です。
アクション拡張は jp.co.canon_soft.wp.runtime.IoActionExtension というインターフェースクラスを元に実装する必要があります。

public interface IoActionExtension {
    void execute(final IoActionExtensionContext context) throws Exception;
}

また、引数になっている jp.co.canon_soft.wp.runtime.IoActionExtensionContext には次のメソッドが実装されています。

修飾子と型メソッドと説明
StringgetActionCode()
呼び出し元のアクション項目コードを取得します
StringgetActionIndex()
呼び出し元のアクションが一覧形式のレベル2である場合に、その何行目(先頭行0)から呼び出されたかを取得します。
なお、レベル1の場合は常に0です
HttpServletRequestgetRequest()
リクエストパラメータやHTTPヘッダーの情報を取得する場合に利用します。
HttpServletResponsegetResponse()
リダイレクトやファイルのダウンロードする場合などに利用します。
DmTransactiongetActiogetTransactionnCode()
データベースのトランザクションを取得します。
MapcallBP()
アクションの加工式に定義したビジネスプロセスを実行します。
戻り値の Map#get(“データモデル項目コード")でビジネスプロセスの戻り値が取得できます。
voidnextIO()
アクションの次入出力に定義した画面に遷移します。
voidnextIO(String ioCode, List nextIoParams)
入出力コードで指定した画面に遷移します。
また、次入出力パラメータを同時に指定することも可能です。
voidfail()
アクションの失敗をシステムに通知します。
アクション実行結果のログ出力などに利用します。

説明はこれくらいにして、次に Java プログラム( ext.MultiDownloadAction )を示します。
Java ファイルの場所は、{プロジェクトディレクトリ}/src/JavaWebApp/@COMMON/WEB-INF/src/ext/MultiDownloadAction.java です。少々長いですが、内容は後述します。

package ext;

import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;

import jp.co.canon_soft.wp.runtime.IoActionExtension;
import jp.co.canon_soft.wp.runtime.IoActionExtensionContext;
import jp.co.canon_soft.wp.runtime.dm.DmCommand;
import jp.co.canon_soft.wp.runtime.dm.DmCondition;
import jp.co.canon_soft.wp.runtime.dm.DmUtilFactory;
import jp.co.canon_soft.wp.runtime.file.DFileUtils;
import wpapp.dm.DmCmdFILES;
import wpapp.dm.DmFILES;
import wpapp.io.IoACTION_IOWrapper;

public class MultiDownloadAction implements IoActionExtension {

    @Override
    public void execute(IoActionExtensionContext context) throws Exception {
        // 加工式に定義しているのビジネスプロセス(BP_ACTION)を呼び出す
        context.callBP();

        Path zipFile = null;
        try {
            // zipファイルを作成
            zipFile = getZipFile(context);
            if (zipFile == null) {
                context.nextIO();
                return;
            }
            // ダウンロードファイル名
            String dlFilename = zipFile.getFileName().toString().replaceAll("\\.zip.+$", ".zip");

            HttpServletResponse response = context.getResponse();
            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition", "attachment; filename=\"" + dlFilename + "\"");

            InputStream input = Files.newInputStream(zipFile);
            IOUtils.copy(input, response.getOutputStream());
        } catch (Exception ex) {
            throw ex;
        } finally {
            if (zipFile != null) {
                Files.delete(zipFile);              
            }
        }
    }

    // zipファイルを作成して返却する
    private Path getZipFile(IoActionExtensionContext context) throws IOException {

        // 選択チェックされている行のレコードを取得する
        Collection<DmFILES> records = getSelectedRecords(context);

        // レコードに含まれるDFileを物理ファイルに変換
        List<Path> paths = dFiletoFile(records);

        // zipファイル作成
        Path zipFile = compressFiles(paths);

        return zipFile;
    }

    // 選択チェックされている行のレコードを取得する
    private Collection<DmFILES> getSelectedRecords(IoActionExtensionContext context) {
        DmCommand cmd = new DmCmdFILES(context.getTransaction());
        DmCondition cond = DmUtilFactory.getDmUtil().createCondition();

        // IOラッパーから、チェックされているレコードの入出力項目(ID)を取得する
        IoACTION_IOWrapper io = IoACTION_IOWrapper.get_Io();
        for (int idx = 0; idx < io.get_G_Length(); idx++) {
            if (io.is_G_Selected(idx)) {
                BigDecimal id = io.getId(idx);

                // データモデルの抽出条件を作成する
                DmCondition subcond = DmUtilFactory.getDmUtil().createCondition();
                subcond.addEqualTo("iD", id);
                cond.addOrCondition(subcond);
            }
        }

        // 抽出条件に基づいてデータモデルのリストを取得する
        return cmd.select(cond);
    }

    // DFileを物理ファイルに変換して、リストを返却
    private List<Path> dFiletoFile(Collection<DmFILES> records) throws IOException {
        List<Path> paths = new ArrayList<Path>();
        for (DmFILES dmFiles : records) {
            Path pdfPath = null;
            try {
                String fileName = dmFiles.getF_PDF_FNM_();
                pdfPath = Files.createTempFile(fileName, ".tmp");
                // DFileの物理ファイル変換
                DFileUtils.createFile(dmFiles.getF_PDF(), pdfPath.toFile());
                paths.add(pdfPath);
            } catch (IOException ex) {
                for (Path path : paths) {
                    Files.delete(path);
                }
                throw ex;
            }
        }
        return paths;
    }

    // zipファイル生成メソッド
    private Path compressFiles(List<Path> paths) throws IOException {
        String prefix = new SimpleDateFormat("yyyyMMddHHmmss").format(new Timestamp(System.currentTimeMillis()));

        Path zipFile = null;
        try {
            zipFile = Files.createTempFile(prefix + ".zip", ".tmp");
            try (OutputStream fos = Files.newOutputStream(zipFile, StandardOpenOption.WRITE);
                    BufferedOutputStream bos = new BufferedOutputStream(fos);
                    ZipOutputStream zos = new ZipOutputStream(bos, Charset.forName("UTF-8"));) {

                // 選択されたリストの画像をzipファイルに埋め込む
                for (Path path : paths) {
                    byte[] data = Files.readAllBytes(path);
                    ZipEntry zip = new ZipEntry(path.getFileName().toString().replaceAll("\\.pdf.+$", ".pdf"));
                    zos.putNextEntry(zip);
                    zos.write(data);
                }

            } catch (IOException ex) {
                throw ex;
            }
        } catch (IOException ex) {
            throw ex;
        } finally {
            for (Path path : paths) {
                Files.delete(path);
            }
        }
        return zipFile;
    }
}

コードの説明です。
39行目、このアクションに定義していた加工式のビジネスプロセス( BP_ACTION )を呼び出しています。
callBP()が正常終了すると、トランザクションがコミットされるので、アクション拡張側でもデータベースの状態を更新する必要がある場合は、コミットされたあとの状態であるという点に注意してください。

44行目、zipファイルを作成するメソッドを呼び出しています。

52行目~57行目、作成したzipファイルを呼び出されたブラウザに HTTP のレスポンスとして返却するための処理を書いています。
HTTP の応答は一つしか返せないので、こういった処理の場合は、次入出力への遷移などは出来ないので注意してください。

68行目~80行目(getZipFile)、zipファイル作成のために、複数子メソッドを呼び出すための親メソッドです。

83行目~102行目(getSelectedRecords)、IOラッパーによって画面の状態を取得することが出来るので、選択している行を特定し、特定した行からテーブルのキー項目(今回は ID という名称)の値を取得して、データモデルのリストを取得しています。
この方法は、データモデルが定義されているテーブルに対して、SQL を発行せずに SELECT 実行を行う方法として、拡張ビジネスプロセスでも利用が出来ます。

105行目~123行目(dFiletoFile)、getSelectedRecords で特定できたレコードから、PDF が格納されているBLOB型の項目を参照し、物理ファイルに変換しています。

126行目~155行目(compressFiles)、dFiletoFile で変換した物理ファイルの PDF を zipファイルに圧縮しています。

アプリケーションを自動生成する

プログラムの作成まで出来たら、Web Performer でアプリの自動生成を行ってみましょう。
メニューバーから、ダイヤのマークをクリックします。

ダイアログが表示されたら、生成対象アプリケーションを選んで、終了ボタンを押すだけです。

アプリケーションを実行してみる

それでは、ブラウザからアプリケーションを実行してみましょう。
アプリケーションサーバー(Tomcat)を起動して、 http://localhost:8080/ACTION_DEMO/ にアクセスします。
ログイン画面が表示されますので、適当な値を入力してログインしてください。

画面一覧から、「ファイルダウンロード拡張」をクリックします。

アニメーションを示します。動作をご覧ください。
複数のファイルを選択して、zipファイルとしてダウンロードできたのが確認出来ます。

注意点

上に示した画面でお気づきかもしれませんが、ファイルのダウンロードは出来ているのですが、画面上の 最終ダウンロード時間や、ダウンロード回数はビジネスプロセスで定義したのにもかかわらず、更新されていません。

これは、HTTP の仕様上、一つのリクエストには一つのレスポンスしか返せないため、ファイルのダウンロードというレスポンスと、更新後の画面という二つのレスポンスは同時に返す事が出来ないためです。
実際はテーブルの更新が行われているので、画面を更新すると次のように項目が更新されているのがわかります。

このような仕様上の特性を踏まえた上で、実装しなければならない点はご注意ください。

最後に

Web Performer V2.4 の新機能であるアクション拡張を使った例を紹介しました。 今回の例のように、拡張性を持ったダウンロードプログラムを作ることや、サーバー処理で取得した値を元に、リダイレクトURLにパラメータを付与して、他のアプリケーションに遷移したり、あるいは標準機能ではまかなえないような条件で画面遷移をさせるということが、この機能によって実現できます。

ただし、アクション拡張に限らず、Java や JavaScript などで独自の拡張実装の量が増えることは、通常のスクラッチ開発と同様に、プログラムレベルでの単体テストが必要になったり、後々の保守性にも課題が出ることが想定されます。安易に独自の拡張プログラムを採用するのでは無く、Web Performer の標準機能で代替できないかなどを考慮して開発することが、超高速開発ツールの特徴を活かせるということを認識しておいてください。