2016-02-03

Java でメッセージダイジェストを生成 (2)

Java でメッセージダイジェストを生成 (2016-01-31) で紹介した GUI プログラムは、本ブログに掲載するためになるべく短いプログラムであることを意識するあまり、肝心な使い心地まで考えたものではありませんでした。これを反省し、もう少し手を加えました。つまり、確かに 1.5 GB 程度の iso ファイルのメッセージダイジェストを計算する時間は、許容できる=我慢できる程度のものだったかもしれませんが、それでも計算中は GUI が反応しなくなります。せっかくスレッドを制御する仕組みが用意されているので、今回は、メッセージダイジェストを計算する部分を別スレッドで処理するように変更しました。

動作を確認した環境は次の通りです。

  • OS: Fedora 23 (x86_64)
  • Java: Java SE 1.8.0_72 (jdk1.8.0_72-1.8.0_72-fcs.x86_64)
  • IDE: NetBeans IDE 8.1

JavaFX で単発的に別スレッドの処理をさせる時には、Task クラスを利用します。参考サイト [1] にこのクラスの使い方が丁寧に説明されています。しかし、別スレッドの call メソッドから返す値をどのようにして本スレッドで受け取ればよいかは、きちんと説明がされていません。もちろん、void setOnSucceeded(EventHandler<WorkerStateEvent> value) メソッドの説明があるのだから、そのぐらいは気づけよと言われるかもしれません。

読む側としては、最初に例を上げて長々と Task の説明があるのですから、もう一歩踏み込んだ説明や例があればいいのにと愚痴りたくなります。クラスやメソッドの説明を読み込んで深く理解する努力をせずに(手っ取り早く)いつも参考にしてしまう Stack Overflow にぴったりな質疑があったので、参考サイト [2] にリンクを載せました。

さて今回は、実行例を最初に紹介しましょう。以下のようになります(画像をクリックすると大きくなります)。

プログラムを以下に示しました。参考サイト [3] の GitHub にイメージを含む全ソースをアップしてあります。

リスト:HashCalc.java 
package hashcalc;

import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.geometry.HPos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.RowConstraints;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

public class HashCalc extends Application {

    Stage stage;
    String baseDir = getClass().getResource("image/").toString();
    Image icoApp = new Image(baseDir + "Binary.png");
    Image icoFile = new Image(baseDir + "File.png");
    Image icoPen = new Image(baseDir + "Pencil.png");

    File file;
    Button button_file;
    Button button_sha256;
    TextField field_file;
    TextField field_sha256;

    @Override
    public void start(Stage primaryStage) {
        this.stage = primaryStage;

        GridPane root = new GridPane();
        root.setId("pane");

        ColumnConstraints col1 = new ColumnConstraints();
        ColumnConstraints col2 = new ColumnConstraints(600);
        ColumnConstraints col3 = new ColumnConstraints();
        root.getColumnConstraints().addAll(col1, col2, col3);

        RowConstraints row1 = new RowConstraints();
        row1.setFillHeight(true);
        RowConstraints row2 = new RowConstraints();
        row2.setFillHeight(true);
        root.getRowConstraints().addAll(row1, row2);

        // File Name label
        Label label_file = new Label("File Name");
        GridPane.setHalignment(label_file, HPos.RIGHT);
        root.add(label_file, 0, 0);

        // File Name field
        field_file = new TextField();
        field_file.setEditable(false);
        GridPane.setHalignment(field_file, HPos.LEFT);
        root.add(field_file, 1, 0);

        // File Name button
        button_file = new Button();
        button_file.setGraphic(new ImageView(icoFile));
        button_file.setOnAction((ActionEvent t) -> {
            FileChooser fileChooser = new FileChooser();
            file = fileChooser.showOpenDialog(stage);
            field_file.setText(file.toString());
            clearMessageDigest();
        });
        root.add(button_file, 2, 0);

        // SHA-256 label
        Label label_sha256 = new Label("SHA-256");
        GridPane.setHalignment(label_sha256, HPos.RIGHT);
        root.add(label_sha256, 0, 1);

        // SHA-256 field
        field_sha256 = new TextField();
        field_sha256.setEditable(false);
        GridPane.setHalignment(field_sha256, HPos.LEFT);
        root.add(field_sha256, 1, 1);

        // SHA-256 button
        button_sha256 = new Button();
        button_sha256.setGraphic(new ImageView(icoPen));
        button_sha256.setOnAction((ActionEvent t) -> {
            try {
                calcMessageDigest("SHA-256", field_sha256);
            } catch (NoSuchAlgorithmException | IOException ex) {
                Logger.getLogger(HashCalc.class.getName()).log(Level.SEVERE, null, ex);
            }
        });
        root.add(button_sha256, 2, 1);
         
        Scene scene = new Scene(root);
        scene.getStylesheets().add(getClass().getResource(getClass().getSimpleName() + ".css").toExternalForm());

        setAppTitle();
        stage.setResizable(false);
        stage.getIcons().add(icoApp);
        stage.setScene(scene);
        stage.show();
    }

    void clearMessageDigest() {
        field_sha256.setText("");
    }

    void calcMessageDigest(String algorithm, TextField field) throws NoSuchAlgorithmException, IOException {
        CalcMessageDigest md = new CalcMessageDigest(file, algorithm);
        md.setOnSucceeded((WorkerStateEvent t) -> {
            field.setText(md.getValue());
            field.requestFocus();
            setAppTitle();
            button_file.setDisable(false);
            button_sha256.setDisable(false);
        });

        Thread th = new Thread(md);
        th.setDaemon(true);
        th.start();

        setAppTitleRun();
        button_file.setDisable(true);
        button_sha256.setDisable(true);
    }

    void setAppTitle() {
        stage.setTitle(getClass().getSimpleName());
    }

    void setAppTitleRun() {
        stage.setTitle(getClass().getSimpleName() + " - calculating");
    }

    public static void main(String[] args) {
        launch(args);
    }
}

今回は、CSS ファイルを加えて少し修飾していますが、TextField の文字サイズは(たぶん)反映されていません。やり方が間違っているかもしれません。

リスト:HashCalc.css 
#pane {
    -fx-padding: 5px; 
    -fx-hgap: 5px;
    -fx-vgap: 2px;
}

.textfield {
    -fx-font-size: 12px;  
}

.button {
    -fx-padding: 3px; 
    -fx-border-insets: 0px;
    -fx-border-width: 0.5px;
    -fx-border-color: black gray gray black;
    -fx-border-radius: 8px;
    -fx-background-radius: 8px;
}

メッセージダイジェストの計算を別スレッドで処理するようにしました。今回はハッシュ値のアルファベットを小文字にして返すようにしました。

リスト:CalcMessageDigest.java 
package hashcalc;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javafx.concurrent.Task;
import javax.xml.bind.DatatypeConverter;

/**
 * CalcMessageDigest
 *
 * @author bitwalk
 */
public final class CalcMessageDigest extends Task<String> {

    File file;
    String algorithm;

    public CalcMessageDigest(File file, String algorithm) throws NoSuchAlgorithmException, IOException {
        this.file = file;
        this.algorithm = algorithm;
    }

    @Override
    protected String call() throws Exception {
        return calcMD();
    }

    String calcMD() throws NoSuchAlgorithmException, FileNotFoundException, IOException {
        if (isEmpty(file)) {
            return "[empty!]";
        }
        if (!checkBeforeReadfile(file)) {
            return "[The file is not readable]";
        }

        MessageDigest md = MessageDigest.getInstance(algorithm);
        FileInputStream in = new FileInputStream(file);

        byte[] dataBytes = new byte[1024];

        int nread = 0;
        while ((nread = in.read(dataBytes)) != -1) {
            md.update(dataBytes, 0, nread);
        }
        byte[] mdbytes = md.digest();

        return DatatypeConverter.printHexBinary(mdbytes).toLowerCase();
    }

    boolean isEmpty(File value) {
        return value == null || value.length() == 0;
    }

    static boolean checkBeforeReadfile(File read) {
        if (read.exists()) {
            if (read.isFile() && read.canRead()) {
                return true;
            }
        }

        return false;
    }
}

参考サイト

  1. Task (JavaFX 8)
  2. How to use the return value of call method of Task class in Javafx - Stack Overflow
  3. bitwalk123/HashCalc

 

0 件のコメント: