画像からExif情報を抜き出すJavaプログラム

Java

スマートフォンやデジタルカメルで撮影した画像のメタ情報として、Exifという情報があります。
このExif情報には、画像を取った位置情報や画像の確度といった、様々なメタ情報が格納されています。


今回は、このExif情報をJavaで取得する方法を紹介していきます。


環境情報


Java:Java1.8.0_281


Exif情報の取得プログラム


Exif情報の取得サンプルプログラムは以下になります。
出力結果が解り易いように、出力結果を「Exif IFD0」「Exif SubIFD」「GPS」の3つのカテゴリ毎に出力をおこなっています。


サンプルプログラムではExif情報を「Metadata」に格納し、「getDirectories()」でメタ情報を取得していきます。
Exif情報の代表的な以下3つの情報を、標準出力しています。


項目

Tagクラスのメソッド

出力内容

タグタイプ

getTagType()

Exif情報のID

タグ名

getTagName()

タグのタイプ(名称)

説明

getDescription()

設定内容


import java.io.File;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.Tag;

public class Exif {

    /** Exif IFD0 */
    private static final String STR_IFD = "Exif IFD0";

    /** Exif SubIFD */
    private static final String STR_SUBIFD = "Exif SubIFD";

    /** GPS */
    private static final String STR_GPS = "GPS";

    // メイン処理
    public static void main(String[] args){

        System.out.print("start: Exif\r\n");

        try {

            /** 対象ファイルを読み込み */
            File file = new File("./sample.jpg");
            Metadata metadata = 
                ImageMetadataReader.readMetadata(file);

            /** ディレクトリ情報でループ */
            for (Directory directory : metadata.getDirectories()) {
                String dirName = directory.getName();

                /** Exif IFD0 */
                if (STR_IFD.equals(dirName)) {
                    System.out.print(
                        "------directoryName=" + dirName + 
                        "------\r\n");
                    for (Tag tag : directory.getTags()) {
                        String tagName = tag.getTagName();
                        int tagType = tag.getTagType();
                        String desc = tag.getDescription();
                        System.out.print(
                            Integer.toString(tagType) + ":" + 
                            tagName + "=" + desc + "\r\n");
                    }
                }

                /** Exif SubIFD */
                if (STR_SUBIFD.equals(dirName)) {
                    System.out.print(
                        "------directoryName=" + dirName + 
                        "------\r\n");
                    for (Tag tag : directory.getTags()) {
                        String tagName = tag.getTagName();
                        int tagType = tag.getTagType();
                        String desc = tag.getDescription();
                        System.out.print(
                            Integer.toString(tagType) + ":" + 
                            tagName + "=" + desc + "\r\n");
                    }
                }

                /** GPS */
                if (STR_GPS.equals(dirName)) {
                    System.out.print(
                        "------directoryName=" + dirName + 
                        "------\r\n");
                    for (Tag tag : directory.getTags()) {
                        String tagName = tag.getTagName();
                        int tagType = tag.getTagType();
                        String desc = tag.getDescription();
                        System.out.print(
                            Integer.toString(tagType) + ":" + 
                            tagName + "=" + desc + "\r\n");
                    }
                }
            }
        } catch (Exception e) {
            System.out.print("occured exception \r\n");
        }

        System.out.print("end: Exif\r\n");
    }
}

Exif情報の取得例


Exif情報の取得例を以下に説明します。
今回は、ネットで公開しているExif情報が格納されている画像に対してサンプルプログラムを実行しています。


Exif情報を確認すると、画像を撮影した位置情報について格納されていることが確認できます。
一番下に出力されている「0:GPS」~「27:GPS」の情報になります。



start: Exif
------directoryName=Exif IFD0------
270:Image Description=SA390025
271:Make=KDDI-SA
272:Model=W51SA
274:Orientation=Top, left side (Horizontal / normal)
282:X Resolution=72 dots per inch
283:Y Resolution=72 dots per inch
296:Resolution Unit=Inch
306:Date/Time=2008:07:08 15:32:08
531:YCbCr Positioning=Center of pixel array
------directoryName=Exif SubIFD------
33437:F-Number=f/3.0
36864:Exif Version=2.20
36867:Date/Time Original=2008:07:08 15:32:08
36868:Date/Time Digitized=2008:07:08 15:32:08
37121:Components Configuration=YCbCr
37378:Aperture Value=f/2.8
37379:Brightness Value=1
37381:Max Aperture Value=f/2.8
37382:Subject Distance=0.0 metres
37383:Metering Mode=Average
37384:White Balance=Unknown
37385:Flash=Flash did not fire, auto
37386:Focal Length=4.8 mm
40960:FlashPix Version=1.00
40961:Color Space=sRGB
40962:Exif Image Width=1600 pixels
40963:Exif Image Height=1200 pixels
41729:Scene Type=Directly photographed image
41985:Custom Rendered=Normal process
41986:Exposure Mode=Auto exposure
41987:White Balance Mode=Auto white balance
41988:Digital Zoom Ratio=1
41989:Focal Length 35=28 mm
41990:Scene Capture Type=Standard
41992:Contrast=None
41993:Saturation=None
41994:Sharpness=None
41996:Subject Distance Range=Unknown
------directoryName=GPS------
0:GPS Version ID=2.200
1:GPS Latitude Ref=N
2:GPS Latitude=35° 42' 2.91"
3:GPS Longitude Ref=E
4:GPS Longitude=139° 45' 57.52"
18:GPS Map Datum=WGS-84
27:GPS Processing Method=65 83 67 73 73 0 0 0 71 80 83 45 70 73 88
end: Exif


JavaでNULL判定・空文字判定をおこなう場合に注意するポイント

Java

Javaで文字列のNULL判定や空文字判定をおこなう場合、判定ロジックに気を付けないと思いもよらない例外が発生する場合があります。

今回は、NULL判定や空文字判定をおこなう場合の注意すべきポイントをサンプルプログラム付きで説明していきます。


環境情報


  • Java:Java1.8.0_281

NULL判定


文字列の比較はStringクラスのequalsを使用しますが、equalsメソッドを使用する変数を比較元にするか?もしくは、比較先にする?によって、例外の発生パターンが異なってきます。

public class NullSample {

    // NULLの定数
    private static String DEFINE_NULL = null;

    // 空文字の定数
    private static String DEFINE_KARA = "";

    public static void main(String[] args){
        System.out.print("start: main\r\n");


        /*------------------------------------------*/
        /* NULL判定                             */
        /*------------------------------------------*/

        // 1.NULL判定/定数が左側
        try {
            String val = "abc";
            if (DEFINE_NULL.equals(val)) {
                System.out.print("val is null \r\n");
            } else {
                System.out.print("val is not null \r\n");
            }
        } catch (Exception e) {
            System.out.print("NULL判定で例外!定数が左側 \r\n");
        }

        // 2.NULL判定/定数が右側
        try {
            String val = "abc";
            if (val.equals(DEFINE_NULL)) {
                System.out.print("val is null \r\n");
            } else {
                System.out.print("val is not null \r\n");
            }
        } catch (Exception e) {
            System.out.print("NULL判定で例外!定数が右側 \r\n");
        }
        System.out.print("end: main\r\n");
    }
}

プログラムの実行結果は以下になります。


start: main
NULL判定で例外が発生!定数が左側  ← 1の判定結果
val is not null                    ← 2の判定結果
end: main

1の判定について、例外が発生しています。
equalsメソッドを使用するオブジェクト自体がNULLの場合は例外が発生するということがわかります。


そのため、実際のプログラムでは、NULLが格納される可能性のある変数ではequalsメソッドは使用しないことが推奨されます。


空文字判定


次は空文字の判定についてです。
サンプルプログラムで動きを確認してみます。


public class KaraSample {

    // NULLの定数
    private static String DEFINE_NULL = null;

    // 空文字の定数
    private static String DEFINE_KARA = "";

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        /*------------------------------------------*/
        /* 空判定                             */
        /*------------------------------------------*/

        // 1.空判定/定数が左側
        try {
            String val = "abc";
            if (DEFINE_KARA.equals(val)) {
                System.out.print("val is kara \r\n");
            } else {
                System.out.print("val is not kara \r\n");
            }
        } catch (Exception e) {
            System.out.print("空判定で例外が発生!定数が左側 \r\n");
        }

        // 2.空判定/定数が右側
        try {
            String val = "abc";
            if (val.equals(DEFINE_KARA)) {
                System.out.print("val is kara \r\n");
            } else {
                System.out.print("val is not kara \r\n");
            }
        } catch (Exception e) {
            System.out.print("空判定で例外が発生!定数が右側 \r\n");
        }
        System.out.print("end: main\r\n");
    }
}

val is not kara    ← 1の判定結果
val is not kara    ← 2の判定結果

1と2のどちらの方法であっても、例外が発生せずに空文字判定をおこなうことができています。


上記のサンプルプログラムは、判定結果が空文字ではないという結果になっていますが、試しに空文字での判定をおこなってみます。


public class KaraSample {

    // NULLの定数
    private static String DEFINE_NULL = null;

    // 空文字の定数
    private static String DEFINE_KARA = "";

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        /*------------------------------------------*/
        /* 空判定                             */
        /*------------------------------------------*/

        // 1.空判定/定数が左側
        try {
            String val = "";
            if (DEFINE_KARA.equals(val)) {
                System.out.print("val is kara \r\n");
            } else {
                System.out.print("val is not kara \r\n");
            }
        } catch (Exception e) {
            System.out.print("空判定で例外が発生!定数が左側 \r\n");
        }

        // 2.空判定/定数が右側
        try {
            String val = "";
            if (val.equals(DEFINE_KARA)) {
                System.out.print("val is kara \r\n");
            } else {
                System.out.print("val is not kara \r\n");
            }
        } catch (Exception e) {
            System.out.print("空判定で例外が発生!定数が右側 \r\n");
        }
        System.out.print("end: main\r\n");
    }
}

val is kara    ← 1の判定結果
val is kara    ← 1の判定結果

正常に、空文字の判定がおこなわれており、結果も正常であることがわかります。



ClientHelloとServerHelloをJavaのDebugログで確認する

Java

SSLハンドシェイクにおいて基本となるのは、通信処理の最初に実施する『ClientHello(クライアントハロー)』と『ServerHello(サーバハロー)』です。


今回は、このSSLハンドシェイクの挙動をJavaのDebugログで出力して、ClientHelloとServerHelloの動きを確認していこうと思います。


環境情報


  • OS:Windows10
  • Java:Java1.8.0_281

SSLハンドシェイクの実験プログラム


SSLハンドシェイクを確認するために、Javaの実験プログラムを作成して、実行結果をログ出力してみます。
実験プログラムは、ポータルサイトである「Excite」にアクセスしているのみです。


import java.io.FileOutputStream;
import java.io.PrintStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class HTTPReq {

    public static void main(String[] args){

        try {
            FileOutputStream fos = new FileOutputStream("out.txt");
            PrintStream ps = new PrintStream(fos);
            System.setOut(ps); 

            System.out.print("start: main\r\n");

            // 1回目コネクション
            URL url = new URL("https://www.excite.co.jp/");
            HttpURLConnection con = null;

            con = (HttpURLConnection) url.openConnection();
            con.setRequestMethod("POST");
            con.connect();

            int statusCode = con.getResponseCode();
            System.out.print("statusCode=" + Integer.toString(statusCode) + "\r\n");

        } catch (Exception e) {
            System.out.print("例外が発生!");
            e.printStackTrace();
        } finally {
        }
        System.out.print("end: main\r\n");
    }
}

Javaの実行バッチは以下になります。


@echo on
set CLASSPATH=.
java -Djavax.net.debug=all HTTPReq >> result.txt 2>&1

Javaのデバッグログは、debug=all」オプションを付けることで出力されます。

-Djavax.net.debug=all

Javaの標準出力は「out.txt」に出力し、デバッグログは「result.txt」に出力しています。


ClientHello


ClientHelloからServerHelloの流れは、SSLハンドシェイクの開始部分となります。
開始部分であっても、Javaのデバッグログには大量の情報が出力されます。


ClientHelloで、クライアントからサーバに対して送信するデータに以下があります。

  • SSL/TLSのバージョン
  • CipherSuiteのリスト
  • セッションID
  • ランダム値
  • 圧縮アルゴリズム
  • TLS拡張情報

実際に出力されるのは以下のような情報となります。

"ClientHello": {
  "client version"      : "TLSv1.2",
  "random"              : "XXX",
  "session id"          : "",
  "cipher suites"       : "[TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384(0xC02C), ・・・]",
  "compression methods" : "00",
  "extensions"          : [
    "server_name (0)": {
      type=host_name (0), value=www.excite.co.jp
    },
    "supported_groups (10)": {
      "versions": [secp256r1, ・・・]
    },
    "ec_point_formats (11)": {
      "formats": [uncompressed]
    },
    "signature_algorithms (13)": {
      "signature schemes": [ecdsa_secp256r1_sha256, ・・・]
    },
    "signature_algorithms_cert (50)": {
      "signature schemes": [ecdsa_secp256r1_sha256, ・・・]
    },
    "extended_master_secret (23)": {
      
    },
    "supported_versions (43)": {
      "versions": [TLSv1.2, TLSv1.1, TLSv1]
    }
  ]
}

SSL/TLSのバージョンのバージョンは、クライアントで使用できるTLSバージョンをサーバに通知しています。
TLS1、1.1、1.2などがあります。


CipherSuiteのリストは、クライアントで使用できるCipherSuiteをリストとしてサーバに通知しています。
このログ以前にも様々な情報が出力されるのですが、クライアントで使用できるCipherSuiteのリストを算出し、サーバに通知します。


セッションIDはクライアント側で保持しているセッションのIDです。
複数の通信をおこなう場合、クライアントはセッションで保持している情報を使用してSSLハンドシェイクをおこないます。
セッションIDを使用した場合、SSLハンドシェイクの手順をスキップすることができます。


ServerHello


ClientHelloに対するサーバ側のレスポンスがServerHelloになります。


ServerHelloで、サーバからクライアントに対して送信するデータには以下の種類があります。


  • 合意したSSL/TLSのバージョン
  • ランダム値
  • セッションID
  • 合意したCipherSuite
  • 圧縮アルゴリズム
  • TLS拡張情報

"ServerHello": {
  "server version"      : "TLSv1.2",
  "random"              : "XXX",
  "session id"          : "YYY",
  "cipher suite"        : "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(0xC02F)",
  "compression methods" : "00",
  "extensions"          : [
    "renegotiation_info (65,281)": {
      "renegotiated connection": []
    },
    "server_name (0)": {
      
    },
    "ec_point_formats (11)": {
      "formats": [uncompressed, ansiX962_compressed_prime, ansiX962_compressed_char2]
    },
    "extended_master_secret (23)": {
      
    }
  ]
}

SSL/TLSのバージョンのバージョンは、クライアント側から提案したTLSのバージョンに対するサーバ側の返却になります。
このバージョンが一致しなければ、SSLハンドシェイクは成立しません。
サーバ側はTLS1.2で要求しているのにサーバ側がTLS1.0のみを許可している、といった場合がそのパターンになります。


合意したCipherSuiteは、クライアント側から提案したCipherSuiteのリストを受信したサーバ側が、そのリストから使用するCipherSuiteを決定します。


最終的に、ServerHelloDoneが出力されれば、ClientHelloからServerHelloの一連の動きは成功となります。


javax.net.ssl|FINE|01|main|2021-04-24 23:11:34.992 JST|ServerHelloDone.java:151|Consuming ServerHelloDone handshake message (

)


Javaの起動オプションにGCログ設定とヒープダンプを設定する

Java

Javaを用いたプログラムで、運用開始後に原因不明の障害が発生する場合は多々あります。


原因不明のメモリリーク、CPUの一時的な極端な上昇といった現象は、アプリケーションのログからはなかなか原因が突き止められない場合があります。
こういった事象に対応するために、GCログの出力と、OutOfMemory発生時のヒープダンプ出力をあらかじめ仕込んでおくと、原因の早期発見が期待できます。


今回は、Javaアプリケーションの起動設定について、GCログとヒープダンプの出力設定をおこなう方法について紹介します。


環境情報


  • Java 1.8.0_281

GCログの出力設定


GCログの出力設定は、Javaの起動オプションとして設定します。
設定オプションは以下になります。


 

-verbose:gc

GCログを出力する。

-Xloggc

GCログの出力先を設定する。

例)-Xloggc:./gc.log-%t

 

ファイル名に設定できる変数要素としては以下の2つがあります。

%t=ログファイルを作成した日時

%p=JavaプロセスのID

-XX:+UseGCLogFileRotation

GCログのローテーションをおこないます。

-XX:+PrintGCDateStamps

GCログに時間のタイムスタンプを表示します。

-XX:+PrintGCDetails

GCログの詳細を出力します。

-XX:NumberOfGCLogFiles

GCログのローテーションをおこなう際、ローテーションするファイルの数を定義します。

-XX:GCLogFileSize

1つのログファイルのファイルサイズを定義します。

定義したファイルサイズに到達したタイミングで、ローテーションがおこなわれます。

定義したファイルサイズに到達したタイミングで、ローテーションがおこなわれます。

 

M=メガ

k=キロ(最小は7K

 


設定例としては以下になります。
実行するプログラムは「GcSample」となり、10キロのGCログファイルを最大5ファイルでローテーションをおこなっています。
GCログファイルにはログファイルを出力した日時を設定するために「%t」オプションを定義していますが、以下はWindowsのバッチファイル例なので、”%%”と記載してエスケープしています。


java -Xms256m -Xmx256m -verbose:gc -Xloggc:./gc.log-%%t -XX:+UseGCLogFileRotation -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10k GcSample 

この定義でログファイルを出力した場合は、以下のような定義となります。


gc.log-2021-02-11_15-52-26.0
gc.log-2021-02-11_15-52-26.1
gc.log-2021-02-11_15-52-26.2
gc.log-2021-02-11_15-52-26.3
gc.log-2021-02-11_15-52-26.4.current ← 出力中ファイル

5ファイルでローテーションされています。


出力されたファイルはGCViewerなどのビューアで参照するのが一般的になります。


ヒープダンプ(HeadDump)の出力設定


GCログを設定しておくことも大事なのですが、GCログだけではOutOfMemoryが発生した場合の、リーク箇所などについては詳細を判断することはできません。

ヒープダンプを設定しておくと、OutOfMemory発生時にヒープダンプ(HeadDump)が出力されるので、詳細の原因調査が可能となります。


-XX:+HeapDumpOnOutOfMemoryError

OutOfMemoryが発生した場合にヒープダンプを出力します。

-XX:HeapDumpPath

ヒープダンプの出力先フォルダ、および、ファイル名を指定します。

ファイル名を指定しない場合は、「java_pidxxx.hprof」といったファイル名になります。

xxxはプロセスIDです。

-XX:OnOutOfMemoryError

OutOfMemoryが発生した場合に実行するコマンドを定義することができます。

実行するコマンドは、シングルクォーテーションで囲んで定義します。

 

例)-XX:OnOutOfMemoryError=”cmd string.”


OutOfMemoryが発生してヒープダンプを出力した際、指定したフォルダにダンプファイルを出力するのですが、同名ファイルが存在する場合は上書きしてくれません。
つまり、同じプロセスIDのJavaプロセスで複数回のOutOfMemoryが発生した場合、一番最初のヒープダンプだけが残ります。


一番最初のヒープダンプを残すのではなく、一番最後のヒープダンプを残したい場合は、OnOutOfMemoryErrorオプションを使います。
以下の例では、ヒープダンプが発生したタイミングで出力したダンプファイルをバックアップフォルダに移動をおこないます。
この定義をおこなうことで、ヒープダンプ出力先フォルダにはダンプファイルは残らず、バックアップフォルダに最後のヒープダンプが残ることになります。


java -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./ -XX:OnOutOfMemoryError="D:/gc/gc-move.bat" GcSample 

OnOutOfMemoryErrorオプションに定義したgc-move.batの中身は以下になります。
backというフォルダに、出力したヒープダンプファイルを強制上書きコピーしています。

move /Y java*.hprof ./back/

OnOutOfMemoryErrorオプションに定義したコマンドは、ヒープダンプが出力された後に実行されるようです。


①:OutOfMemoryが発生

②:ヒープダンプを出力

③:OnOutOfMemoryErrorオプションに定義したコマンドを実行


このような動きであることを考慮して、OnOutOfMemoryErrorオプションに定義したコマンドを検討する必要があります。


仮に、最初のヒープダンプファイル・最後のヒープダンプファイル、という事ではなく、ヒープダンプの履歴を取りたい(全てのヒープダンプファイルを残しておきたい)場合は、上記例の「gc-move.bat」を修正すれば、様々な定義をおこなうことができます。
しかし、ヒープダンプファイルはプロセスのヒープ情報をそのままダンプしているので、かなりサイズが大きいファイルとなります。
そのため、ダンプファイルの履歴をとることはおすすめはしません。



JavaのSupplierを使って汎用的なリトライ処理を実装する

Java

アプリケーションの機能を実装していく中で、様々なリトライ処理が必要になってきます。
DBアクセスに失敗した場合のリトライ、HTTP通信(APIなど)に失敗した場合のリトライ、など様々かと思います。


Javaでこういったリトライ処理を実装しようとすると、普通にやるとfor文やwhile文を使ってしまいがちです。
それでもいいですが、Javaのユーティリティクラス群であるSupplierを使えば、もうちょっと汎用的に実装することができます。


今回は、Javaのユーティリティクラスである「java.util.function.Supplier」を使って、汎用的なリトライ処理を実装する方法を紹介していきます。


環境情報


  • OS:Windows10
  • Java:Java1.8

ユーティリティクラス群で使用されている関数型インターフェースは、Java8から導入された機能となります。
Java1.7以前は使用できないので注意してください。


java.util.functionは関数型インターフェース


java.util.function配下の機能は全て、関数型インターフェースで実装されています。
イメージとしては、実行するメソッドをパラメータとして渡す形です。


java.util.functionのAPI仕様をみると複雑なインターフェースが沢山あるようにみえるのですが、大きくわけて4つに分類されます。
今回のリトライ処理では「Supplier」を使うのですが、他3つのインターフェースは以下になります。


種類

実装するメソッド

概要

Function<T,R>

R apply(T t)

実装するメソッドは、引数としてTを受け取り、結果としてRを返すものになる

Consumer<T>

void accept(T t)

実装するメソッドは、引数としてTを受け取り、結果を返さず終了するものになる

Predicate<T>

boolean test(T t)

実装するメソッドは、引数としてTを受け取り、boolean値を結果として返すものになる

Supplier<T>

T get()

実装するメソッドは、何も引数として受け取らず、結果としてTを返すものになる


関数型インターフェースの簡単なサンプルを、以下に紹介します。
先ほど説明した通り、関数をパラメータとして渡して渡された側で関数を実行しています。


return new RetryUtil()
    .retryFunc(() -> {
        ※実行する機能
    );
public class RetryUtil {
    private <T> T retryFunc(Supplier<T> func) {
        return func.get();
    }
}

リトライ処理


リトライ処理は、java.util.function配下の「Supplier」を使用します。
「Supplier」を使用し、実施する処理自体は呼び出し元で定義し、リトライの仕組み自体は共通クラス(ユーティリティクラス)に実装しています。


まずはリトライ処理を実施するメイン処理です。

import java.util.function.Supplier;

/**
    リトライ処理サンプル
*/
public class RetrySample {

    /**
        リトライ処理メイン
    */
    public static void main(String[] args) 
        throws InterruptedException {
        System.out.print("start: RetrySample\r\n");

        // 処理結果を失敗で初期化
        boolean isDone = false;

        // リトライ処理
        try {
            isDone = retryExec();
        } catch (Exception e) {
            System.out.print("RetrySample: main is failer\r\n");
        } finally {
            System.out.print(
                "RetrySample: result is " + isDone + "\r\n");
        }

        System.out.print("end: RetrySample\r\n");
    }

    /**
        リトライ処理の実施
    */
    private static boolean retryExec () 
        throws InterruptedException {

        // 1.リトライ回数とスリープ時間を設定
        int retryNum = 3;
        long sleepMilSec = 1000;

        // 2.ユーティリティを使って〇〇〇処理を実施する
        return new RetryUtil(retryNum, sleepMilSec)
            .retryFunc(() -> {
                try {
                    System.out.print("retryExec\r\n");
                    boolean isSucess = 〇〇〇処理を実施
                    if (isSucess) {
                        return true;
                    } else {
                        throw new RetryException();
                    }
                } catch (Exception e) {
                    throw new RetryException();
                }
            }, "RetrySample");
    }
}

「1.リトライ回数とスリープ時間を設定」で、リトライユーティリティのオブジェクトに設定する初期値を定義しています。
今回はリトライ処理ということで、ありがちなリトライ回数とスリープ時間はユーティリティクラスのコンストラクタで設定できようにしています。
その他、初期値として設定したい値があるのであれば、同様にコンストラクタで定義するのがよいかと思います。


「2.ユーティリティを使って〇〇〇処理を実施する」の機能が、実際に関数型インフェースで定義しているリトライ処理する中身です。
サンプルなので「〇〇〇処理を実施」としていますが、実際にはここにリトライ制御したい機能を実装していくのがよいでしょう。
リトライする契機としては、独自例外クラスである「RetryException」が発生した場合になります。
失敗したら(isDoneがfalseだったら)「RetryException」をスローすることで、リトライ処理を実行するようにユーティリティクラスを準備しています。


次にリトライユーティリティクラスです。

import java.text.MessageFormat;
import java.util.function.Function;
import java.util.function.Supplier;

/**
    リトライユーティリティ
*/
public class RetryUtil {

    // リトライ回数
    private final int retryNum;

    // リトライ時のスリープ時間
    private final long retryWait;

    /**
     * コンストラクタ
     */
    public RetryUtil(int retryNum, long retryWait) {

        // 1.リトライ回数とスリープ時間を設定
        this.retryNum = retryNum;
        this.retryWait = retryWait;
    }

    /**
     * リトライ処理
     */
    public <T> T retryFunc(Supplier<T> func, String funcName)
            throws RetryException, InterruptedException {
        return retryFunc(func, funcName, 1, null);
    }

    /**
     * リトライユーティリティ
     */
    private <T> T retryFunc(Supplier<T> func, 
        String funcName, int count, Exception befEx)
        throws RetryException, InterruptedException {

        // 2.Supplierで提供されたfuncを実行
        try {
            return func.get();

        // 3.例外が発生した場合はリトライ回数に
        //     達するまで自身を再度呼び出す
        } catch (RetryException e) {
            System.out.print("RetryException\r\n");
            if (count > this.retryNum) {
                System.out.print("retry end\r\n");
                throw new RetryException(e);
            }
            count++;
            Thread.sleep(this.retryWait);
            System.out.print("next retry " + 
                Integer.toString(count) + "回目の処理開始\r\n");
            return retryFunc(func, funcName, count, null);
        }
    }
}

「1.リトライ回数とスリープ時間を設定」で、リトライ回数とスリープ時間をクラス変数として格納しています。
実際のリトライ処理時に、この値を参照しています。


「2.Supplierで提供されたfuncを実行」で、実際に関数型インターフェースで渡されたメソッドを実行しています。
実行方法は、パラメータ名.get() で実行されます。


「3.例外が発生した場合はリトライ回数に達するまで自身を再度呼び出す」がリトライ処理のメインとなる部分ですね。
個別例外である「RetryException」が発生するとリトライ処理を実施します。
リトライ回数が上限に達すると上位に例外をスローしてリトライ処理を中断します。
上限に達していない場合は再度自身を呼び出して同じ処理を実行しています。


最後に個別の例外クラスです。
リトライ処理を制御しやすくするために、個別例外を準備しています。


/**
    リトライユーティリティ固有例外
*/
public class RetryException extends RuntimeException {

    /**
     * コンストラクタ
     */
    public RetryException() {
        super();
    }

    /**
     * コンストラクタ(例外指定)
     * @param cause 例外
     */
    public RetryException(Throwable cause) {
        super(cause);
    }
}


Apacheでクライアント認証をおこない、Javaプログラムで証明書情報を取得して解析

apache

Apacheを使ったWEBアプリケーションでSSL通信をおこなうためには、Apacheが認識するサーバのフォルダ上に証明書を格納し、Apacheの設定ファイルに証明書の情報を定義する必要があります。

かつ、より安全なSSL通信をおこなう方法は、クライアント認証をおこなう方法になります。

許可した端末のブラウザにクライアント証明書をインストールし、そのクライアント証明書がインストールされている端末についてのみSSL通信を許可します。


今回は、クライアント証明書を使ってSSL通信をおこなうための設定方法と、クライアント証明書から情報を抜き出すJavaプログラムについて説明します。


環境情報


  • HTTPサーバ:Apahce2.2.15
  • 言語:Java1.7.79

SSL認証の概要


クライアント証明書を用いたSSL通信は、通常のSSL通信と少し異なります。

SSL通信をおこなう端末のブラウザにクライアント証明書をインストールしておき、通信においてブラウザのクライアント証明書と、サーバ側のルート証明書とで証明書のマッチング認証をおこない、同証明書であればSSL通信を許可します。



上記のクライアント証明書を用いたSSL通信をおこなうためには、ブラウザへのクライアント証明書のインストールと、Apacheへの証明書設定の2つが必要になります。


Apacheへの証明書設定(サーバ側)


サーバ側の証明書設定は、通常、証明書発行局が作成した証明書をサーバ上に指定フォルダに格納し、Apacheの設定ファイルに証明書のパスを設定します。
OpenSSLを使って独自で証明書を作成した場合でも設定方法は同様です。


設定するファイルは「ssl.conf」になります。
設定箇所は以下の4つです。


SSLCertificateFile

サーバ証明書の格納パス

SSLCertificateKeyFile

秘密鍵の格納パス

SSLCertificateChainFile

サーバー証明書の中間証明書格納パス

SSLCACertificateFile

クライアント証明書のルート証明書格納パス


# サーバー証明書
SSLCertificateFile /etc/httpd/cert/server.crt

# 秘密鍵
SSLCertificateKeyFile /etc/httpd/cert/secret.key

# サーバー証明書における中間証明書
SSLCertificateChainFile /etc/httpd/cert/publicKey.cer

# クライアント証明書におけるルート証明書
SSLCACertificateFile /etc/httpd/cert/root.pem

設定する証明書によっては、証明書の形式も意識する必要があります。
例えば、クライアント証明書におけるルート証明書(SSLCACertificateFile)は、テキスト形式でなくてはなりません。

Apacheのバージョンによるかもしれないのですが、少なくとも「Apahce2.2.15」ではバイナリ形式のルート証明書は認識できません。


Apacheのエラーログに、以下のログが出力されてApacheの起動に失敗します。


[Thu Mon DD HH:MI:SS YYYY] [info] SSL Library Error: 336105671error:140890C7:SSL routines:SSL3_GET_CLIENT_CERTIFICATE:peer did not  return a certificate No CAs known to server for verification? 

証明書発行局からテキスト形式の証明書を発行してもらうことが一番よいのですが、OpenSSLコマンドを用いてバイナリからテキスト形式への変更も可能です。


openssl x509 -inform der -in root.cer -outform pem -out root.pem 

ブラウザへの証明書インストール(クライアント側)


ブラウザにインストールするクライアント証明書も、サーバ証明書と同様に証明書発行局が作成した証明書をインストールします。

こちらも基本的には、ウィザードに沿ってブラウザに証明書をインストールしていきます。


証明書ファイル(.der、など)をクリックすると、以下のようなインストールウィザードがひらきます。

表示内容を確認し、OKボタンを押下します。



保管場所を選択します。

通常であれば「現在のユーザ」でよいです。

選択したら次へボタンを押下します。



証明書ストアを設定します。

何かしらの理由がなければ、自動ストアを選択して、次へボタンを押下します。


以下のウィンドウが表示されれば、無事にクライアント証明書のインストール完了です。



インストールが完了したら、ブラウザの設定ウィンドウをひらいてインストール結果を確認します。

Edge(Chromium版)での確認方法は、設定ウィンドウをひらき、「プライバシーとサービス」→「証明書の管理」で証明書情報を確認できます。



「個人」タブにインストールした証明書が表示されていれば、正常にクライアント証明書が認識されているという事になります。


証明書から情報を取得


クライアント証明書を使ったSSL通信において、WEBアプリケーション側でクライアント証明書を情報を取得する事も可能です。
具体的には、JavaプログラムでHTTPリクエストから抽出します。


以下のプログラムでは、java.security.cert.X509Certificateを使って、HttpServletRequestから識別情報を取得しています。


private void getCert(HttpServletRequest request) {

    // -- クライアント証明書情報の取得 --//
    java.security.cert.X509Certificate[] certs = 
        (java.security.cert.X509Certificate[]) request.getAttribute(
        "javax.servlet.request.X509Certificate");

    // クライアント証明書からクライアント証明書識別子を取得
    Principal principal = certs[0].getSubjectX500Principal();

    // クライアント証明書識別子を取得
    X509Name name = new X509Name(principal.getName());
    Vector<?> cert = 
        name.getValues(X509ObjectIdentifiers.commonName);
    if (cert.size() == 1) {
        String code = cert.get(0);
    }

上記のプログラムで、クライアント証明書に格納されている「commonName」を取得することができます。


上記プログラムの用途としては、インストールされているクライアント証明書に格納されている情報(commonName)毎にアプリケーションの制御を変える場合です。
クライアント証明書Aをインストールしているブラウザからログインしたユーザと、クライアント証明書Bをインストールしているブラウザからログインしたユーザとで、上記で取得した情報をもとに制御の切替が可能となります。



Javaで配列をコピーする。シャーロンコピーとディープコピーの違いとは?

Java

Javaで配列をコピーする際、気を付けなければいけないポイントがあります。
シャーロンコピーとディープコピーの違いです。


この2つの挙動の違いを把握しておかないと、思わぬバグを埋め込んでしまうことなります。


今回は、シャーロンコピーとディープコピーの違いを、サンプルプラグラムをもとに説明していきます。


環境情報


  • OS:Windows10
  • Java:1.8.0_60

シャーロンコピーとは?ディープコピーとは?


シャーロンコピーとディープコピーとは何でしょうか?

まずは、この2つのコピーの違いについて説明します。


シャーロンコピー


C言語を経験した人の場合は、アドレスコピーときくとピンとくるかと思います。
要素を指定するアドレスをポインタを使用してコピーしており、要素がもつ値はコピーしていないです。


ディープコピー


値コピー、と言い換えることができます。
シャーロンコピーとは異なり、アドレスではなく、要素そのものをコピーします。


シャーロンコピーの概念が理解し辛い人が多い気がします。
C言語を使った人はわかるかと思いますが、「ポインタ」のイメージですね。


シャーロンコピーとディープコピーのサンプルプログラム


それでは、さっそくですがサンプルプログラムです。

例として、以下のサンプルプログラムになります。

  • コピー対象は、独自エントリクラス(Entry)の配列を「ArrayList」で保持する。
  • シャーロンコピー→ディープコピーの順番でコピーを実施。
  • コピー元配列の内容を表示。

サンプルプログラム


作成するクラスは2つになります。
「CopyTest.java」がコピーするメインクラス、「Entry.java」が配列要素となるエントリクラスです。


CopyTest.java

import java.util.ArrayList;

public class CopyTest {

    // メイン処理
    public static void main(String[] args){
        System.out.print("start: CopyTest\r\n");

        /**
        ①:エントリ内容の作成と確認
        */
        ArrayList<Entry> srcList = new ArrayList<Entry>();
        Entry newEnt1 = new Entry();
        newEnt1.setSei("Yamada");
        newEnt1.setNa("Taro");
        srcList.add(newEnt1);
        Entry newEnt2 = new Entry();
        newEnt2.setSei("Suzuki");
        newEnt2.setNa("jiro");
        srcList.add(newEnt2);
        System.out.print("エントリ内容の作成確認\r\n");
        dispList(srcList);

        /**
        ②:シャーロンコピー
        */
        ArrayList<Entry> shtList = srcList;
        Entry entry = shtList.get(0);
        entry.setSei("Satou");
        entry.setNa("Saburo");
        System.out.print("リストをコピー確認\r\n");
        dispList(srcList);

        /**
        ③:ディープコピー
        */
        ArrayList<Entry> deList = 
            new ArrayList<Entry>(srcList.size());
        for (Entry ent : srcList) {
            deList.add(ent.clone());
        }
        entry = deList.get(0);
        entry.setSei("Tanaka");
        entry.setNa("Shiro");
        System.out.print("リストをコピー確認\r\n");
        dispList(srcList);

        System.out.print("end: CopyTest\r\n");
    }

    private static void dispList(ArrayList<Entry> dispList){
        for (Entry dispEnt : dispList) {
            System.out.print("sei = " + dispEnt.getSei() + "\r\n");
            System.out.print("na = " + dispEnt.getNa() + "\r\n");
        }
    }
}

Entry.java

public class Entry implements Cloneable {
    private String sei;
    private String na;

    // 姓の設定
    public void setSei(String sei) {
        this.sei = sei;
    }

    // 名の設定
    public void setNa(String na) {
        this.na = na;
    }

    // 姓の取得
    public String getSei() {
        return this.sei;
    }

    // 名の取得
    public String getNa() {
        return this.na;
    }

    // クーロン
    public Entry clone() {
        try {
            return (Entry) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

シャーロンコピーの実行結果


サンプルプログラムの②の部分がシャーロンコピーのサンプルプログラムです。
シャーロンコピーを実行した結果は以下になります。

sei = Satou
na = Saburo
sei = Suzuki
na = jiro

実際にシャーロンコピーしている部分は以下です。

ArrayList<Entry> shtList = srcList;

シャーロンコピーしたコピー先リスト(shtList)の内容を変更しています。
エントリの1つ目について、以下のように変更しています。

・Sei=Satou 
・Na=Saburo

シャーロンコピーしたコピー先リスト(shtList)を変更したにも関わらず、コピー元のリスト(srcList)が変更されています。
これはなぜかというと、コピーしたのはあくまでアドレスであるシャーロンコピーをおこなっているからになります。
変更先リストを変更しているにも関わらず変更元リストが変更されている、という事です。


ディープコピーの実行結果


サンプルプログラムの③の部分がディープコピーのサンプルプログラムです。
ディープコピーを実行した結果は以下になります。

sei = Satou
na = Saburo
sei = Suzuki
na = jiro
end: CopyTest

実際のディープコピーは、「Entry」クラスのcloneメソッドになります。

ArrayList<Entry> deList = new ArrayList<Entry>(srcList.size());
for (Entry ent : srcList) {
    deList.add(ent.clone());
}

要素を完全にコピーしています。
完全にコピーした要素に対して変更しているので、変更元の配列要素は変更されていないです。


これがディープコピーになります。
通常は、上記のようにcloneメソッドを準備して、要素を完全コピーする実装を準備しておきます。


よく、様々なウェブサイトで以下のように実装すればディープコピーされると記載されているのですが、それは間違いです。
気を付けてください。

ArrayList<Entry> deList = new ArrayList<Entry>(srcList);

まとめ


ディープコピーとシャーロンコピーの違いを正しく理解してプログラミングしましょう!


よくあるのが、ディープコピーしていると思っていたがシャーロンコピーで、コピー元の要素が変更されてしまった!というパターンです。
思わぬバグを作りこんでしまう事になります。



FTPサーバとの通信をJavaでおこなうサンプルプログラム

Java

システムで外部システムと連携する方法はいろいろありますが、FTPサーバを使ってファイルのやり取りで連携するパターンも多々あります。


FTPサーバを使ってファイルのやり取りをおこなう場合、プログラムからFTPサーバにログインして、ファイルのアップロードやダウンロードをおこなう必要が出てきます。


今回は、Javaを使ってFTPサーバとのファイル送受信をおこなうサンプルプログラムを紹介します。


環境情報


  • OS:Windows10
  • Java:Java1.8.0_60
  • FTPサーバ:IIS(インターネットインフォメーションサービス)

JavaでFTPを使うために、「commons-net-3.6.jar」を使用しています。


FTPサーバについては、以前の記事で作成したIIS(インターネットインフォメーションサービス)を使います。



FTPサンプルプログラム


サンプルプログラムを紹介します。
サンプルプログラムのクラスは2つです。


FTPに対する操作をおこなうクラスとして「FtpMng.java」を準備し、「FtpMng.java」のインスタンスを使って処理をおこなうクラスが「FtpSample.java」になります。


以下のクラスがFTPに対する操作をおこなうクラスである「FtpMng.java」です。
FTPに対する操作一つ一つに対してメソッドを準備しています。


FTPサーバに対しておこなっているのは、ファイルのアップロード(FTPサイトにファイルを置く)と、ファイルのダウンロード(FTPサイトからファイルをもらう)の2つになります。

import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.SocketException;

public class FtpMng {

    private FTPClient   cli;

    // コンストラクタ
    public FtpMng() {
    }

    // 接続
    public boolean connect() {
        int rep;

        System.out.print("start: connect\r\n");
        try {
            cli = new FTPClient();
            cli.setDataTimeout(60000);
            cli.connect("localhost");
            rep = cli.getReplyCode();
            if (!FTPReply.isPositiveCompletion(rep)) {
                return false;
            } 
            System.out.print("end: connect\r\n");
            return true;
        } catch (SocketException e) {
            return false;
        } catch (IOException ie) {
            return false;
        }
    }

    // ログイン
    public boolean login() {
        System.out.print("start: login\r\n");
        try {
            if (!cli.login("ftp", "password")) {
                return false;
            }
            System.out.print("end: login\r\n");
            return true;
        } catch (IOException ie) {
            return false;
        }
    }

    // ダウンロード
    public void downLoad() {
        FileOutputStream    outputstream;
        boolean             isRetrieve;

        System.out.print("start: downLoad\r\n");
        try {
            outputstream =
                new FileOutputStream("download.txt");
            isRetrieve =
               cli.retrieveFile("download.txt", outputstream);
            outputstream.close();
            if (!isRetrieve) {
                System.out.print("error: downLoad\r\n");
            }
            System.out.print("end: downLoad\r\n");
            return;
        } catch (IOException ie) { 
            return;
        }
    }

    // アップロード
    public void upLoad() {
        FileInputStream     inputstream;
        boolean             isStore;

        System.out.print("start: upLoad\r\n");
        try {
            inputstream =
                new FileInputStream("upload.txt");
            isStore =
                cli.storeFile("upload.txt",
                inputstream);
            inputstream.close();
            if (!isStore) {
                System.out.print("error: upLoad\r\n");
            } 
            System.out.print("end: upLoad\r\n");
            return;
        } catch (IOException ie) {
            return;
        }
    }

    // 切断
    public boolean disConnect() {

        System.out.print("start: disConnect\r\n");
        try {
            if (cli != null && cli.isConnected()) {
                cli.disconnect(); 
            }
            System.out.print("end: disConnect\r\n");
            return true;
        } catch (IOException ie) {
            return false;
        }
    }
}

以下のクラスが実際にFTP処理をおこなう「FtpSample.java」です。
「FtpMng.java」に準備しているメソッドを順番に呼び出しているだけです。

public class FtpSample {

    public static void main(String[] args){
        System.out.print("start: FtpSample\r\n");
        FtpMng ftpClient = new FtpMng();

        // 1:接続
        ftpClient.connect();

        // 2:ログイン
        ftpClient.login();

        // 3:ダウンロード
        ftpClient.downLoad();

        // 4:アップロード
        ftpClient.upLoad();

        // 5:切断
        ftpClient.disConnect();

        System.out.print("end: FtpSample\r\n");
    }
}

FTPサンプルプログラムの解説


それでは以下に解説していきます。
そんなに難しいプログラムではないですが。


接続・ログイン・切断


FTPサーバに対してファイルをアップロード・ダウンロードする前に、FTPサーバにログインしなければいけません。
手順としては、接続→ログイン、の順番になります。


まずは接続です。

cli = new FTPClient();
cli.setDataTimeout(60000);
cli.connect("localhost");
rep = cli.getReplyCode();
if (!FTPReply.isPositiveCompletion(rep)) {
    return false;
} 

正常に接続できたかどうか?については、getReplyCode()で返り値を取得し、FTPReplyクラスで返り値を検証することで判断することができます。


次にログインです。

if (!cli.login("ftp", "password")) {
    return false;
}

IDとパスワードを指定してログインを実行です。
これでFTPサーバにログインできます。


ID ftp
パスワード password

一連の処理をおこなった後に切断して処理終了です。

if (cli != null && cli.isConnected()) {
    cli.disconnect(); 
}

サンプルプログラムでは、一応、FTPClientインスタンスが存在しており、かつ、接続されている場合に切断しています。


ダウンロード


FTPサーバからファイルを取得します。

outputstream =
    new FileOutputStream("download.txt");
isRetrieve =
    cli.retrieveFile("download.txt", outputstream);
outputstream.close();

Javaのアウトプットストリームクラスを使用してFTPサーバからファイルをダウンロードしています。
ストリームのクローズは忘れずに、ですね。


アップロード


FTPサーバにファイルを格納します。


inputstream =
    new FileInputStream("upload.txt");
isStore =
    cli.storeFile("upload.txt",
        inputstream);
inputstream.close();

こちらはダウンロードとは逆の方式です。
Javaのインプットストリームクラスを使用してFTPサーバにファイルをアップロードしています。
こちらも、ストリームのクローズは忘れずに、です。


まとめ


FTPサーバに対するファイル操作は、Javaで実装することが可能です。
サンプルプログラムでは必要最低限のソースコードしか紹介していませんが、FTPコマンドで可能な操作はJavaが全て可能です。


まとめ
  • FTPサーバに対する操作は、Javaで全て実装可能。
  • サンプル以外のFTP操作についても可能



JavaMailを使って添付ファイル付きメールの送受信をおこなう

Java

JavaMailを使ってメールの送信や受信をおこなうことができますが、添付ファイル付きメールの送信や受信もおこなうことができます。


今回は、添付ファイル付きメールの送受信について、サンプルプログラムを紹介します。
添付ファイルを付けてメールの送信をおこなうサンプルプログラムと、メールを受信して添付ファイルを保存するサンプルプログラムになります。


添付ファイルを意識していないサンプルプログラムについては、以前に作成した記事を参照ください。


環境情報


今回は、Yahooメールを使ってメールの送受信をおこなってみます。


  • Java1.8.60
  • POP3:Yahooメール
  • SMTP:Yahooメール

添付ファイル付きメールを受信する


メール受信プログラムは以下になります。
1~4が、添付ファイル保存で重要な部分になります。


import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileOutputStream;

import java.util.Properties;

import javax.mail.BodyPart;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.internet.MimeMultipart;

public class MailReceiveAtach {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        try {
            // メール受信のプロパティ設定
            Properties props = new Properties();
            props.put("mail.pop3.host", "pop.mail.yahoo.co.jp");
            props.put("mail.pop3.port", "110");

            // メール受信フォルダをオープン
            Session session = Session.getDefaultInstance(props);
            Store store = session.getStore("pop3");
            store.connect("address", "password");
            Folder folderInbox = store.getFolder("INBOX");
            folderInbox.open(Folder.READ_ONLY);

            // メッセージ一覧を取得
            Message[] arrayMessages = folderInbox.getMessages();
            for (int lc = 0; lc < arrayMessages.length; lc++) {

                // メッセージの取得
                Message message = arrayMessages[lc];

                // 件名の取得と表示
                String subject = message.getSubject();
                System.out.print("件名:" + subject + "\r\n");

                // 受信日時を表示
                String sentDate = message.getSentDate().toString();
                System.out.print("受信日時:" + sentDate + "\r\n");

                // 本文の取得と表示
                MimeMultipart mltp = 
                     (MimeMultipart)message.getContent();
                BodyPart body1 = mltp.getBodyPart(0);
                System.out.print("本文:" + 
                    body1.getContent().toString() + "\r\n");

                // 1:添付ファイルボディを取得
                BodyPart body2 = mltp.getBodyPart(1);
                String fileName = body2.getFileName();
                System.out.print(
                    "添付ファイル名:" + fileName + "\r\n");

                // 2:添付ファイルのストリームを取得
                InputStream ins = body2.getInputStream();

                // 3:出力ストリームに出力しながらファイルを保存する
                OutputStream outs = 
                    new FileOutputStream("save.txt"); 
                int out;
                while ((out = ins.read()) != -1) {
                    outs.write(out);
                }

                // 4:ストリームをクローズ
                ins.close();
                outs.close();

                // 取得の最大件数は10件
                if (lc &amp;amp;gt;= 10) {
                    break;
                }
                System.out.print("\r\n");
            }
        } catch (Exception e) {
            System.out.print("例外が発生!")
            e.printStackTrace();
        } finally {
        }
        System.out.print("end: main\r\n");
    }
}

「1」で、添付ファイルが保存されているボディパートを取得します。
本サンプルプログラムは決め打ちで「1」と記載しています。

本来であれば、ボディパートの数だけループして、添付ファイルが格納されているボディパートについてのみ、「2」以降の添付ファイル保存処理をおこなうべきです。
添付ファイルが複数存在する場合はこのボディパートが複数存在することになるので、ループで回す必要があるということですね。


「2」で、添付ファイルのインプットストリームを取得して、ファイル保存の準備をおこないます。


「3」で、アウトプットストリームを使用して添付ファイルの保存をおこないます。
添付ファイルのファイル名は「save.txt」です。


「4」でストリームをクローズして終了、です。


添付ファイル付きメールを送信する


メール送信プログラムは以下になります。
添付ファイルを2つ添付しているサンプルプログラムになります。


import java.util.Properties;

import javax.mail.Address;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.PasswordAuthentication;
import javax.mail.Transport;
import javax.mail.Multipart;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;
import javax.activation.FileDataSource;
import javax.activation.DataHandler;

public class MailSendMlt {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        try {
            // メール送信のプロパティ設定
            Properties props = new Properties();
            props.put("mail.smtp.host", "smtp.mail.yahoo.co.jp");
            props.put("mail.smtp.port", "587");
            props.put("mail.smtp.auth", "true");
            props.put("mail.transport.protocol", "smtp");
            props.put("mail.smtp.ssl.trust", "*");
            props.put("mail.smtp.starttls.enable", "true");
            props.put("mail.smtp.connectiontimeout", "10000");
            props.put("mail.smtp.timeout", "10000");

            // セッションを作成する
            Session session = Session.getInstance(props,
                new javax.mail.Authenticator() {
                    protected PasswordAuthentication 
                        getPasswordAuthentication() {
                        return new PasswordAuthentication(
                            "fromAddress", "password");
                    }
                });

            // メールの送信先はYahooメール。送信元もYahooメール
            Message message = new MimeMessage(session);
            message.setFrom(new InternetAddress(
                "fromAddress", "fromAddress"));
            message.setReplyTo(
                new Address[]{new InternetAddress("toAddress")});
            message.setRecipients(
                Message.RecipientType.TO, 
                InternetAddress.parse("toAddress"));
            message.setSubject("テスト");

            // メッセージ本文
            MimeBodyPart messageBodyPart = new MimeBodyPart();
            messageBodyPart.setText("テストメール。");

            // 1:添付ファイル1を添付するボディパートを取得
            MimeBodyPart attachedFilePart1 = new MimeBodyPart();

            // 2:添付ファイル1のデータソースを取得
            FileDataSource fs1 = new FileDataSource("attach1.txt");

            // 3:ボディパート1に添付ファイル1を添付
            attachedFilePart1.setDataHandler(new DataHandler(fs1));
            attachedFilePart1.setFileName(
                MimeUtility.encodeWord(fs1.getName()));

            // 4:添付ファイル2を添付するボディパートを取得
            MimeBodyPart attachedFilePart2 = new MimeBodyPart();

            // 5:添付ファイル2のデータソースを取得
            FileDataSource fs2 = new FileDataSource("attach2.txt");

            // 6:ボディパート2に添付ファイル2を添付
            attachedFilePart2.setDataHandler(new DataHandler(fs2));
            attachedFilePart2.setFileName(
                MimeUtility.encodeWord(fs2.getName()));

            // 7:メールに、本文・添付1・添付2の3つを添付
            Multipart multipart = new MimeMultipart();
            multipart.addBodyPart(messageBodyPart);
            multipart.addBodyPart(attachedFilePart1);
            multipart.addBodyPart(attachedFilePart2);
            message.setHeader(
                "Content-Transfer-Encoding", "base64");

            // 8:メールを送信する
            message.setContent(multipart);
            Transport.send(message);

        } catch (Exception e) {
            System.out.print("例外が発生!\r\n");
            e.printStackTrace();
        } finally {
        }
        System.out.print("end: main\r\n");
    }
}

「1」~「3」が添付ファイル1の作成処理になります。


「1」で、添付ファイルのボディパートを作成します。


「2」で、添付するファイルのデータソースを作成します。


「3」で、添付ファイルを添付します。


「4」~「6」も「1」~「3」と同じで、添付ファイル2を添付します。


「7」で、本文・添付1・添付2をメッセージ本文に添付してメール自体は完成します。


最後に「8」でメールを送信します。


まとめ


紹介したサンプルプログラムで、添付ファイルのメール送受信が可能です。
参考にしてください。





POIで罫線を書く。Javaサンプルプログラムと、全ての罫線出力

ApachePOI

JavaでExcelの加工や出力をおこなう時は、Apache POIが便利です。
Apache POIは、セルへの文字列出力や塗り潰し色の設定が可能ですが、罫線の設定も可能です。


今回は、Apache POIで可能な罫線出力を、サンプルプログラムと同時に出力結果をふまえて紹介していきます。


セルの塗り潰し色設定方法については、以前の記事を参考にしてください。



環境情報


  • OS:Windows10
  • Java:Java1.8.0_60
  • Apache POI:poi 4.1.0

罫線を設定するサンプルプログラム


罫線を設定するサンプルプログラムは以下になります。
ApachePOIで設定可能な全ての罫線を設定して出力しています。


import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.CellValue;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

public class PoiLine {

    public static void main(String[] args){
        System.out.print("start: main\r\n");

        Workbook tempbook = null;
        try {
            //--- テンプレートファイルをひらいて、シートを指定 --//
            Path tempPath = Paths.get("./template.xlsx");
            InputStream inSt = Files.newInputStream(tempPath);
            tempbook = new XSSFWorkbook(inSt);
            Sheet sheet = tempbook.getSheet("Sheet1");

            //--- B2セル 罫線なし ---//
            Row row = sheet.createRow(1);
            Cell cell = row.createCell(1);
            cell.setCellValue("罫線なし");
            CellStyle style = tempbook.createCellStyle();
            style = setBorder(style, BorderStyle.NONE);
            cell.setCellStyle(style);

            //--- B4セル 長点線 ---//
            row = sheet.createRow(3);
            cell = row.createCell(1);
            cell.setCellValue("長点線");
            style = tempbook.createCellStyle();
            style = setBorder(style, BorderStyle.DASH_DOT);
            cell.setCellStyle(style);

            //--- B6セル 長点点線 ---//
            row = sheet.createRow(5);
            cell = row.createCell(1);
            cell.setCellValue("長点点線");
            style = tempbook.createCellStyle();
            style = setBorder(style, BorderStyle.DASH_DOT_DOT);
            cell.setCellStyle(style);

            //--- B8セル 破線 ---//
            row = sheet.createRow(7);
            cell = row.createCell(1);
            cell.setCellValue("破線");
            style = tempbook.createCellStyle();
            style = setBorder(style, BorderStyle.DASHED);
            cell.setCellStyle(style);

            //--- B10セル 点線 ---//
            row = sheet.createRow(9);
            cell = row.createCell(1);
            cell.setCellValue("点線");
            style = tempbook.createCellStyle();
            style = setBorder(style, BorderStyle.DOTTED);
            cell.setCellStyle(style);

            //--- B12セル 二重線 ---//
            row = sheet.createRow(11);
            cell = row.createCell(1);
            cell.setCellValue("二重線");
            style = tempbook.createCellStyle();
            style = setBorder(style, BorderStyle.DOUBLE);
            cell.setCellStyle(style);

            //--- B14セル 中太線 ---//
            row = sheet.createRow(13);
            cell = row.createCell(1);
            cell.setCellValue("中太線");
            style = tempbook.createCellStyle();
            style = setBorder(style, BorderStyle.MEDIUM);
            cell.setCellStyle(style);

            //--- B16セル 長点線(中太線) ---//
            row = sheet.createRow(15);
            cell = row.createCell(1);
            cell.setCellValue("長点線(中太線)");
            style = tempbook.createCellStyle();
            style = setBorder(style, BorderStyle.MEDIUM_DASH_DOT);
            cell.setCellStyle(style);

            //--- B18セル 長点点線(中太線) ---//
            row = sheet.createRow(17);
            cell = row.createCell(1);
            cell.setCellValue("長点点線(中太線)");
            style = tempbook.createCellStyle();
            style = setBorder(
                style, BorderStyle.MEDIUM_DASH_DOT_DOT);
            cell.setCellStyle(style);

            //--- B20セル 破線(中太線) ---//
            row = sheet.createRow(19);
            cell = row.createCell(1);
            cell.setCellValue("破線(中太線)");
            style = tempbook.createCellStyle();
            style = setBorder(style, BorderStyle.MEDIUM_DASHED);
            cell.setCellStyle(style);

            //--- B22セル 斜長点線 ---//
            row = sheet.createRow(21);
            cell = row.createCell(1);
            cell.setCellValue("斜長点線");
            style = tempbook.createCellStyle();
            style = setBorder(style, BorderStyle.SLANTED_DASH_DOT);
            cell.setCellStyle(style);

            //--- B24セル 太線 ---//
            row = sheet.createRow(23);
            cell = row.createCell(1);
            cell.setCellValue("太線");
            style = tempbook.createCellStyle();
            style = setBorder(style, BorderStyle.THICK);
            cell.setCellStyle(style);

            //--- B26セル 細線 ---//
            row = sheet.createRow(25);
            cell = row.createCell(1);
            cell.setCellValue("細線");
            style = tempbook.createCellStyle();
            style = setBorder(style, BorderStyle.THIN);
            cell.setCellStyle(style);

            //--- B28セル 細点線 ---//
            row = sheet.createRow(27);
            cell = row.createCell(1);
            cell.setCellValue("細点線");
            style = tempbook.createCellStyle();
            style = setBorder(style, BorderStyle.HAIR);
            cell.setCellStyle(style);

            //--- out.xlsxとしてファイル出力 --//
            Path outPath = Paths.get("./out.xlsx");
            OutputStream outSt = Files.newOutputStream(outPath);
            tempbook.write(outSt);
        } catch (IOException e) {
            System.out.print("入出力例外が発生!");
        } finally {
            //--- テンプレートをクローズ --//
            try {
                if (tempbook != null) {
                    tempbook.close();
                }
            } catch (IOException e) {
                System.out.print("終了処理で入出力例外が発生!");
            }
        }
        System.out.print("end: main\r\n");
    }

    // 
    // 罫線を設定する
    // 
    public static CellStyle setBorder(
        CellStyle cellStyle, BorderStyle kind) {
        cellStyle.setBorderLeft(kind);
        cellStyle.setBorderRight(kind);
        cellStyle.setBorderTop(kind); 
        cellStyle.setBorderBottom(kind); 
        return cellStyle;
    }
}

実際に罫線を設定している処理は、「setBorder」メソッドです。
罫線は上下左右を別々にしか設定できないため、上下左右全ての罫線を指定された罫線種別に変更するメソッドを自作しています。


setBorderLeft()

セル左の罫線を設定

setBorderRight()

セル右の罫線を設定

setBorderTop()

セル上の罫線を設定

setBorderBottom()

セル下の罫線を設定


罫線の種類


ApachePOIでは、エクセルで設定可能な罫線を全て指定可能になります。
罫線の種類はBorderStyleクラスに定義されており、サンプルプログラムで全ての罫線設定を試しています。


NONE

罫線なし

DASH_DOT

長点線

DASH_DOT_DOT

長点点線

DASHED

破線

DOTTED

点線

DOUBLE

二重線

MEDIUM

中太線

MEDIUM_DASH_DOT

長点線(中太線)

MEDIUM_DASH_DOT_DOT

長点点線(中太線)

MEDIUM_DASHED

破線(中太線)

SLANTED_DASH_DOT

斜長点線

THICK

太線

THIN

細線

HAIR

細点線


実際にサンプルプログラムを出力した結果は以下になります。
全ての罫線が出力できています。



まとめ


ApachePOIを使用すれば、簡単にエクセル操作ができます。
今回紹介した罫線の設定も可能ですが、色の設定も可能となります。


まとめ
  • Javaでエクセルを操作するためには、Apache POIをつかうべき!
  • Apache POIを使えば、エクセルへの文字列出力だけではなく、罫線設定も簡単におこなうことができる