2015年1月25日日曜日

[Java][batch]Javaプログラムをバッチファイルから実行する(FilePicker)

別の記事で掲載したAppMainから呼び出す、共通インターフェースを実装する、個別クラスについて記述する。

FileFinderと同じようなクラスだが、ここでは所定のフォルダ配下を再帰的に検索し、指定した名前のファイルを見つけたら、別のディレクトリにコピーするユーティリティクラスをサンプルとする。

FileFinderと本クラスの2クラスのサンプルを記載したのは、同じ1つのバッチ呼び出しの入り口から異なる2つ以上の機能を呼び出すことができることを例示するためである。

<以下、ソースコード>
package cmd;

import java.io.File;

import org.apache.log4j.Logger;

import exception.AppException;

public class FilePicker implements CommandInterface {

    /** ログ */
    private final Logger log = Logger.getLogger(this.getClass());

    /** 結果出力ディレクトリ */
    private File resultDir;

    /**
     * コマンドラインインターフェースです。 <br>
     * 本クラスの機能をバッチから呼び出す場合は、このメソッドを実行します。
     *
     * @param args
     *            コマンドライン引数
     * @throws Exception
     */
    @Override
    public void doMain(String[] args) throws AppException {

        // コマンドライン引数が指定されていない場合は、エラーとする
        if (args == null || args.length == 0) {
            throw new AppException("コマンドライン引数がありません。");
        }

        // 第1引数が自クラスを指していない場合は、エラーとする
        if (!args[0].contains(this.getClass().getName())) {
            throw new AppException("不正な呼び出しです。  args[0] : " + args[0]);
        }

        // 引数が足りない場合は、エラーとする
        String paramName1 = "検索対象ディレクトリ名";
        String paramName2 = "検索するファイル名";
        String paramName3 = "結果出力ディレクトリ名";
        if (args.length < 4) {
            throw new AppException("引数が足りません。  (第1引数 " + paramName1 + ", 第2引数 "
                    + paramName2 + ", 第3引数 " + paramName3 + ")");
        }

        // 引数をログに記録する
        String crlf = System.getProperty("line.separator");
        String msg = "ファイル抽出" + crlf + "\t" + paramName1 + " : " + args[1]
                + crlf + "\t" + paramName2 + " : " + args[2] + crlf + "\t"
                + paramName3 + " : " + args[3];
        log.info(msg);

        // ファイル抽出を実行する
        doPickupFile(args[1], args[2], args[3]);
    }

    /**
     * 検索対象ディレクトリを検索してファイルを抽出します。
     *
     * @param targetDirName
     *            検索対象ディレクトリ名
     * @param searchFileName
     *            検索するファイル名
     * @param resultDirName
     *            結果出力ディレクトリ名
     * @throws AppException
     */
    public void doPickupFile(String targetDirName, String searchFileName,
            String resultDirName) throws AppException {

        // 検索対象ディレクトリが存在しない場合は、エラーとする
        File targetDir = new File(targetDirName);
        if (!(targetDir.exists() && targetDir.isDirectory())) {
            throw new AppException("検索対象ディレクトリが存在しません。  " + targetDirName);
        }

        // 結果出力ディレクトリが存在しない場合は、エラーとする
        resultDir = new File(resultDirName);
        if (!(resultDir.exists() && resultDir.isDirectory())) {
            throw new AppException("検索対象ディレクトリが存在しません。  " + resultDirName);
        }

        // ファイル抽出を実行する
        doPickupFileRecursive(searchFileName, targetDir, resultDir);
    }

    /**
     * ディレクトリを再帰的にたどって、ファイルをコピーします。
     *
     * @param searchFileName
     *            検索するファイル名
     * @param currentDir
     *            カレントディレクトリ
     * @param currentResultDir
     *            カレント結果出力ディレクトリ
     * @throws AppException
     */
    private void doPickupFileRecursive(String searchFileName, File currentDir,
            File currentResultDir) throws AppException {

        // ディレクトリ内にファイルがなければ、処理を終了する
        File[] files = currentDir.listFiles();
        if (files == null || files.length == 0) {
            return;
        }

        // ディレクトリ内の全てのファイルを処理するまでループ
        for (File file : files) {

            if (file.isDirectory()) {

                // ディレクトリの場合は、結果出力先に同じディレクトリを作成する
                File resultDir = new File(currentResultDir.getAbsolutePath()
                        + File.separator + file.getName());
                if (!resultDir.exists()) {
                    resultDir.mkdirs();
                }

                // 本処理を再帰的に呼び出す
                doPickupFileRecursive(searchFileName, file, resultDir);

            } else {

                // ファイルの場合は、パターンにマッチするか判定を行う
                if (!file.getName().matches(searchFileName)) {
                    continue;
                }

                // ファイルが見つかったらログファイルに書き込みを行う
                String destFileName = currentResultDir.getAbsolutePath()
                        + File.separator + file.getName();
                String crlf = System.getProperty("line.separator");
                String msg = "ファイルをコピーします。" + crlf + "\tコピー元 : "
                        + file.getAbsolutePath() + crlf + "\tコピー先 : "
                        + destFileName;
                log.info(msg);

                // ファイルをコピーする
                copyFile(file.getAbsolutePath(), destFileName);
            }
        }
    }

    /**
     * ファイルをコピーします。
     *
     * @param srcFileName
     *            コピー元ファイル名
     * @param destFileName
     *            コピー先ファイル名
     */
    private void copyFile(String srcFileName, String destFileName)
            throws AppException {

        // ファイルエンコード(文字化け)の問題があるため、copyコマンドでコピーする
        String[] cmd = { "cmd", "/c", "copy", srcFileName, destFileName };
        ProcessBuilder pb = new ProcessBuilder(cmd);
        Process process;
        try {
            process = pb.start();
            process.waitFor();

        } catch (Exception e) {
            throw new AppException(e);
        }
    }
}

[Java][batch]Javaプログラムをバッチファイルから実行する(FileFinder)

別の記事で掲載したAppMainから呼び出す、共通インターフェースを実装する、個別クラスについて記述する。

ここでは所定のフォルダ配下を再帰的に検索し、指定した名前のファイルを見つけるユーティリティクラスをサンプルとする。
バッチから呼び出されるクラスを作るときに注意しなければならないことは、メッセージボックスなどのプログラムを「止める」処理を書いてはいけないということである。

また、実際にバッチファイルから実行して何か問題が起きた時、調査は原則としてログから行うより他ない。処理ルートのどこを通ったか分かるようにしておかないと、ログを見ても問題が起きた場所が分からない。

ただしログの量は多くなりすぎないようにすることも重要である。処理を中断せざるを得ない致命的エラーの箇所全てと、正常に処理が進んだときのチェックポイントにログを配置したい。

<以下、ソースコード>
package cmd;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import org.apache.log4j.Logger;

import exception.AppException;

/**
 * ファイル検索
 */
public class FileFinder implements CommandInterface {

    /** ログ */
    private final Logger log = Logger.getLogger(this.getClass());

    /** 結果出力ファイル */
    private BufferedWriter resultFile;

    /**
     * コマンドラインインターフェースです。 <br>
     * 本クラスの機能をバッチから呼び出す場合は、このメソッドを実行します。
     *
     * @param args
     *            コマンドライン引数
     * @throws Exception
     */
    @Override
    public void doMain(String[] args) throws AppException {

        // コマンドライン引数が指定されていない場合は、エラーとする
        if (args == null || args.length == 0) {
            throw new AppException("コマンドライン引数がありません。");
        }

        // 第1引数が自クラスを指していない場合は、エラーとする
        if (!args[0].contains(this.getClass().getName())) {
            throw new AppException("不正な呼び出しです。  args[0] : " + args[0]);
        }

        // 引数が足りない場合は、エラーとする
        String paramName1 = "検索対象ディレクトリ名";
        String paramName2 = "検索するファイル名";
        String paramName3 = "結果出力ファイル名";
        if (args.length < 4) {
            throw new AppException("引数が足りません。  (第1引数 " + paramName1 + ", 第2引数 "
                    + paramName2 + ", 第3引数 " + paramName3 + ")");
        }

        // 引数をログに記録する
        String crlf = System.getProperty("line.separator");
        String msg = "ファイル検索" + crlf + "\t" + paramName1 + " : " + args[1]
                + crlf + "\t" + paramName2 + " : " + args[2] + crlf + "\t"
                + paramName3 + " : " + args[3];
        log.info(msg);

        // ファイル検索を実行する
        doFindFile(args[1], args[2], args[3]);
    }

    /**
     * ファイル検索を行います。
     *
     * @param targetDirName
     *            検索対象ディレクトリ名
     * @param searchFileName
     *            検索するファイル名
     * @param resultFileName
     *            結果出力ファイル名
     * @throws AppException
     */
    public void doFindFile(String targetDirName, String searchFileName,
            String resultFileName) throws AppException {

        // 検索対象ディレクトリが存在しない場合は、エラーとする
        File targetDir = new File(targetDirName);
        if (!(targetDir.exists() && targetDir.isDirectory())) {
            throw new AppException("検索対象ディレクトリが存在しません。  " + targetDirName);
        }

        try {
            // 結果出力ファイルを作成する
            resultFile = new BufferedWriter(new FileWriter(new File(
                    resultFileName)));
            resultFile.write("次のフォルダ配下を検索します。  " + targetDirName);
            resultFile.newLine();
            resultFile.newLine();

            // ファイル検索を実行する
            doFindFileRecursive(searchFileName, targetDir);

        } catch (IOException e) {
            throw new AppException(e);

        } finally {

            try {
                // 最後に必ずファイルを閉じる
                if (resultFile != null) {
                    resultFile.close();
                }
                resultFile = null;

            } catch (IOException e) {
                throw new AppException(e);
            }
        }
    }

    /**
     * ディレクトリを再帰的にたどって、ファイルを検索します。
     *
     * @param searchFileName
     *            検索するファイル名
     * @param currentDir
     *            現在のディレクトリ
     * @throws IOException
     */
    private void doFindFileRecursive(String searchFileName, File currentDir)
            throws IOException {

        // ディレクトリ内にファイルがなければ、処理を終了する
        File[] files = currentDir.listFiles();
        if (files == null || files.length == 0) {
            return;
        }

        // ディレクトリ内の全てのファイルを処理するまでループ
        for (File file : files) {

            if (file.isDirectory()) {

                // ディレクトリの場合は、本処理を再帰的に呼び出す
                doFindFileRecursive(searchFileName, file);

            } else {

                // ファイルの場合は、パターンにマッチするか判定を行う
                if (!file.getName().matches(searchFileName)) {
                    continue;
                }

                // ファイルが見つかったら結果出力ファイルに書き込みを行う
                resultFile.write(file.getParentFile().getAbsolutePath());
                resultFile.newLine();
                resultFile.write("\t" + file.getName());
                resultFile.newLine();
            }
        }
    }
}

[Java][batch]Javaプログラムをバッチファイルから実行する(インターフェース及び例外クラス)

別の記事で掲載したAppMainから呼び出す、共通インターフェースの定義について記述する。
バッチプログラム実行時には、「String[] args」という文字列の配列を可変引数として渡すことができる。
そこから呼び出される機能も、取りうる引数は全て 「String[] args」になる。そのため、共通インターフェースであるCommandInterface.javaも同じ引数とした。

ただしメソッド名の横に「throws」を書かなくても投げられる致命的エラー(いわゆるRuntimeException)以外の例外は、アプリケーションで捕捉して処理したい。
呼び出す個々の機能で、例えばIOExceptionが起きた時、throws Exceptionで何でも全部例外を受け取る側の処理に任せてしまっては、メンテナンスに著しい支障をきたす。
そこで共通インターフェースはAppExceptionという拡張例外をスローする仕様とし、原則として個々のクラス内部で起きるエラーは個々のクラスが処理し、上位にスローすべき例外は拡張例外のみとする。

 <以下、CommandInterface.javaのソースコード>
package cmd;

import exception.AppException;

/**
 * コマンドインターフェース
 */
public interface CommandInterface {

    /**
     * コマンドラインインターフェースです。 <br>
     * 本クラスの機能をバッチから呼び出す場合は、このメソッドを実行する。
     *
     * @param args
     *            コマンドライン引数
     * @throws Exception
     */
    public void doMain(String[] args) throws AppException;
}

<以下、アプリケーション例外クラス>
package exception;

/**
 * アプリケーション例外
 */
public class AppException extends Exception {

    /** シリアルバージョン */
    private static final long serialVersionUID = 1L;

    /**
     * エラーメッセージを渡すタイプのコンストラクタです。
     *
     * @param message
     *            エラーメッセージ
     */
    public AppException(String message) {

        // スーパークラスの処理を実行する
        super(message);
    }

    /**
     * 例外クラスを渡すタイプのコンストラクタです。
     *
     * @param cause
     *            原因
     */
    public AppException(Throwable cause) {

        // スーパークラスの処理を実行する
        super(cause);
    }
}

2015年1月24日土曜日

[Java][batch]Javaプログラムをバッチファイルから実行する(AppMain.java)

別の記事で掲載したバッチファイルから呼び出されるJava側の入り口のクラスを記述する。

AppMainはmainメソッドを持つプログラムエントリポイントとなるクラスである。
ここではCommandInterfaceというインタフェースによって、複数のJavaクラスの呼び分けができるようになっている。

Javaクラスをバッチから呼び出したい場合、全てのクラスにmainを記述するのは移植性の面で問題がある。mainメソッドはstaticであるため、ついついロジック本体のクラスをnewせずにメンバ等も全てstaticに書いてしまいがちである。

入り口はstaticなmainメソッドでもよいが、実際にはそこから複数のロジックを呼び出したい。
そのような場合は、入り口クラスに呼び出したいロジッククラスの完全修飾名を渡し、更に必要な引数も渡せる仕組みが必要である。

入り口が String[] args というパラメータしか取れないので、これと全く同じ戻り値と引数を持つインターフェースを作成し、個々のロジッククラスを呼び出すように記述している。

CommandInterface他のソースコードは、別の記事として掲載する。

<以下、ソースコード>
package app;

import org.apache.log4j.Logger;

import cmd.CommandInterface;
import exception.AppException;

/**
 * プログラムエントリポイント
 */
public class AppMain {

    /** ログ */
    private static Logger log = Logger.getLogger(AppMain.class.getName());

    /**
     * プログラムエントリポイント
     *
     * @param args
     *            コマンドライン引数
     */
    public static void main(String[] args) {

        try {
            // コマンドライン引数が指定されていない場合は、エラーとする
            if (args == null || args.length == 0) {
                throw new AppException("コマンドライン引数がありません。");
            }

            // 第1引数がクラスでない場合は、エラーとする
            CommandInterface ommandInterface = null;
            try {
                Class<?> clazz = Class.forName(args[0]);
                ommandInterface = (CommandInterface) clazz.newInstance();
            } catch (Exception e) {
                writeStarckTraceLog(e);
                return;
            }

            // メイン処理を呼び出す
            ommandInterface.doMain(args);

        } catch (AppException e) {
            writeStarckTraceLog(e);

        } catch (Exception e) {
            writeStarckTraceLog(e);
        }
    }

    /**
     * 例外のスタックトレースをログに記録する。
     *
     * @param e
     *            例外
     */
    private static void writeStarckTraceLog(Exception e) {
        String crlf = System.getProperty("line.separator");
        StringBuffer msg = new StringBuffer(e.getClass().getName() + " "
                + e.getMessage() + crlf);
        StackTraceElement[] elements = e.getStackTrace();
        if (elements != null && elements.length > 0) {
            for (StackTraceElement element : elements) {
                msg.append("\t" + element.toString() + crlf);
            }
        }
        log.error(msg);
    }
}

[Java][log4j]設定ファイルの書き方サンプル

log4jの設定ファイルのサンプルを記述する。
1MBのファイルを最大5ファイルローテーションさせる書き方は、次の通り。

<log4j.properties>
# Cyclic log specification
log4j.appender.LOGFILE=org.apache.log4j.RollingFileAppender
log4j.appender.LOGFILE.MaxFileSize=1MB
log4j.appender.LOGFILE.MaxBackupIndex=5

# log file name
log4j.appender.LOGFILE.File=cmdtool.log

# append mode
log4j.appender.LOGFILE.Append=true

# log level
log4j.appender.LOGFILE.Threshold=DEBUG

# logging pattern
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{yyyy/MM/dd HH:mm:ss.SSS} [%p] %C{1}(%L) %m %n

# output LOGFILE by log level DEBUG
log4j.rootCategory=DEBUG, LOGFILE

<実際に出力されるログのサンプル>
2015/01/24 23:43:13.243 [INFO] TestClass(10) ログ文字列

「TestClass」はログ記録を実施したクラス、横の数字は行番号、「ログ文字列」はログに出力した内容となっている。

[Java][batch]Javaプログラムをバッチファイルから実行する

バッチファイルからJavaを呼び出す方法について記述する。
バッチファイルからJavaコマンドを呼び出す書式は、次の通り。
java -cp [クラスパス] [実行するクラスの完全修飾名] [引数1] [引数2] [引数3]・・・

クラスパスはカレントディレクトリを基準とした相対パスで記述するのが一般的。
Windowsの場合、パス名はセミコロン(;)で区切る。
例えば次のように記述した場合、
java -cp .;bin;lib\log4j.jar

ピリオド(.)はカレントディレクトリを指すので、「カレントディレクトリ」、「binディレクトリ」、「libディレクトリ配下のlog4j.jar」という3つのパスを指定していることになる。
クラスを動かすために必要な全てのクラスパスが通っていないとクラスが見つからないエラーになるので注意を要する。

バッチファイルの内容は、次の通り。 (以下のサンプルでは、絶対パスをバッチファイル内に記述しないために、外部ファイルからバッチ内で使う可変引数を読み込む)
呼び出しているクラスはapp.AppMainというクラスであり、cmd.FileFinderやcmdFilePickerなどのクラスはapp.AppMainから呼び出される個別機能である。(入り口さえ作っておけば、複数機能を同じバッチから呼び出せることを示すためのサンプル)

呼び出しているapp.AppMain他のクラスは、別の記事に掲載する。

<[batch.properties]バッチ内で可変引数として読み込ませる値>
JAVA_CMD=C:\pleiades\java\6\bin\java.exe
LOG4J_JAR=lib\org.apache.log4j_1.2.15.v201012070815.jar

<[doCmd.bat]バッチファイルの内容>
@echo off
rem 外部ファイルから可変引数を呼び出す
for /f "tokens=1,2 delims==" %%a in ('findstr JAVA_CMD batch.properties') do set JAVA_CMD=%%b
for /f "tokens=1,2 delims==" %%a in ('findstr LOG4J_JAR batch.properties') do set LOG4J_JAR=%%b

rem コマンドラインからJavaクラスを呼び出すための変数設定
set CLASS_PATH=.;bin;%LOG4J_JAR%
set APP_MAIN=app.AppMain
set CMD_TOOL=%JAVA_CMD% -cp %CLASS_PATH% %APP_MAIN%

rem ファイル検索コマンド
set FIND_FILE=%CMD_TOOL% cmd.FileFinder

rem ファイル抽出コマンド
set PICK_UP_FILE=%CMD_TOOL% cmd.FilePicker

rem ファイル検索を実施する。
rem 第1引数 : 検索対象ディレクトリ名
rem 第2引数 : 検索するファイル名(正規表現)
rem 第3引数 : 検索結果出力ファイル名
rem %FIND_FILE% "path1" "pattern" "resultfilename"

rem ファイルを抽出する
rem 第1引数 : 検索対象ディレクトリ名
rem 第2引数 : 検索するファイル名(正規表現)
rem 第3引数 : 検索結果出力先フォルダ名
%PICK_UP_FILE% "path1" "pattern" "path2"

pause

2015年1月17日土曜日

[業務知識][生命保険]告知書

 保険契約締結時に、健康状態に問題がないか被保険者に申告してもらうための書類を指す。「がん、心臓病、脳卒中などの病気にかかったことがないか」「過去1年に入院したことがないか」「過去3年以内に手術したことがないか」など、被保険者の健康状態に関する質問で構成される。告知書は保険契約を締結できるかどうかの判断に使われる重要な資料であり、虚偽の申告をすると保険金が受け取れないなどの問題が発生する。

[業務知識][生命保険]引受

 生命保険会社が被保険者との契約を締結することを保険を引き受けるという。
 保険の業務において「引受」とは、保険契約締結の業務全般を指す。引受は保険会社本体が行う。保険会社本体の営業職や保険代理店が営業を行った結果、保険契約を締結するという手順が一般的であるが、近年ではインターネット上から被保険者本人が応募して引受業務が発生することが多くなっている。
 電子計算機システムによる引受は営業のためのコストが大幅に削減できることから、保険料の低減・顧客サービスの向上にもつながるとされ、新たな保険契約の形式として注目されている。

[業務知識][保険]保険者と被保険者、受取人、指定代理人

 「保険者」とは保険を提供する側のことで保険会社のことを指す。「被保険者」とは保険に加入する側のことを指す。これは生命保険、損害保険の別に関わらず、保険業務全般に共通する用語である。ただし生命保険は人に対して保険をかけるが、損害保険は人以外のものに保険をかけるので注意が必要である。たとえば火災保険ならば家、自動車保険ならば車に保険がかかる。損保の場合は補償を受け取る者、という意味で被保険者となる。
 「受取人」は保険金を受け取る人を指す。生命保険の被保険者が死亡した、などの理由により保険金を受け取れる状態となったときに、受取人に指定されている人が保険金を受け取る。従来型の生命保険は被保険者が死亡した場合、残された家族が経済的に困らないように、という意味を込めて通常は配偶者や子供が受取人となっていることが多かった。
 しかし近年では医療保険などのように、被保険者本人が病気になって困ったときお金を受け取れるようにするタイプの保険も多く、その場合は被保険者と受取人が同じになる。また余命を宣告されたときに、生きているうちに保険金を受け取りたい」というニーズに対応し、生前に本人が保険金を受け取ることができる保険も増えてきた。この「生きているうちに」というのは「リビングニーズ特約」と呼ばれ、保険契約の特約として付与できることが多い。
 「指定代理人」とは、本人の代わりに保険金を受け取れる代理人のことである。たとえば医療保険やガン保険など、被保険者と受取人が同じである契約の場合、保険金を受け取れる状態になっても本人の体調が悪くて保険金の請求ができない可能性がある。そのような場合に、指定代理人が本人に代わって保険料を受け取れるようにするための仕組みである。
 かつてはガンであることを本人に知らせずに治療のための保険金を受け取るために使う、という意味もあったようだが、今では指定代理人の仕組みをこのように使うことはまずない。近年、日本はアメリカと同じように急激に訴訟社会化しており、ガンであるのに「ガンではない」と医者が言うことは、裁判で負けるという理由でありえなくなった。かつては「ガン=死」というイメージから、本人への告知はなるべく行わないことも多かったそうだが、現在は末期ガンなら「末期ガンです」と本人に隠さず告知するのが、医療の常識となっている。