Files.walkFileTreeでディレクトリを再帰的に探索するデモコード

# 概要

Javaでディレクトリの再帰的な探索を行う処理は、Java 1.7で導入された`Files.walkFileTree`メソッドを使うと簡単に実装できます。
また、探索したそれぞれのファイル、ディレクトリに対して任意の処理を行うには`FileVisitor`インターフェースを実装したクラスを使います。

**walkFileTree**

第1引数のstartには探索の起点となるpath、第2引数にFileVisitorのインスタンスを渡します。

```java:walkFileTree
public static Path walkFileTree(Path start, FileVisitor<? super Path> visitor) throws IOException {
  // ...
}
```

**環境**

* Windows 10 Professional
* OpenJDK 11.0.2

**参考**

* [Files (Java SE 11 & JDK 11)] (https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/nio/file/Files.html)
* [FileVisitor (Java SE 11 & JDK 11)] (https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/nio/file/FileVisitor.html)

# 再帰的な処理を行うデモコード

このデモコードでは、探索する起点パス以下のファイル、ディレクトリの相対パス（起点パスからの）を取得しListにまとめます。またファイルの場合はMD5でチェックサムを計算します。

## ComputeFileChecksumVisitor

`SimpleFileVisitor`クラスは`FileVisitor`インターフェースを実装した基本的なVisitorクラスです。このクラスを継承してファイル、ディレクトリに対する処理を実装します。

### ファイルに対する操作

オーバーライドした`visitFile`は探索したファイル毎にコールバックされるメソッドで、ここでファイルの相対パス取得とチェックサムを計算する処理を実装しています。

### ディレクトリに対する操作

`preVisitDirectory`メソッドは探索したディレクトリ毎にコールバックされるメソッドです。ディレクトリではチェックサムの計算を行わないので相対パスだけ取得しています。
なお、ディレクトリに対する操作では他に`postVisitDirectory​`というメソッドも用意されていますが、メソッド名に`pre`,`post`と付いているようにどのタイミングでコールバックされるかの違いになります。

### コード

```java:ComputeFileChecksumVisitor
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;

public class ComputeFileChecksumVisitor extends SimpleFileVisitor<Path> {

	private final Path start;
	private final String hashAlg;
	private final List<FileItem> items = new ArrayList<>();

	public ComputeFileChecksumVisitor(Path start, String hashAlg) {
		this.start = start;
		this.hashAlg = hashAlg;
	}

	public List<FileItem> getResult() {
		return items;
	}

	@Override
	public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
		if (!dir.equals(start)) {
			FileItem item = new FileItem(relativePath(dir), "");
			items.add(item);
		}
		return FileVisitResult.CONTINUE;
	}

	@Override
	public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
		FileItem item = new FileItem(relativePath(file), checksum(file));
		items.add(item);
		return FileVisitResult.CONTINUE;
	}

	private Path relativePath(Path path) {
		if (path.startsWith(start)) {
			return path.subpath(start.getNameCount(), path.getNameCount());
		}
		throw new RuntimeException();
	}

	private String checksum(Path path) throws IOException {
		MessageDigest digest = null;
		try {
			digest = MessageDigest.getInstance(hashAlg);
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}
		try (InputStream input = Files.newInputStream(path);
			 DigestInputStream dInput = new DigestInputStream(input, digest)) {
			while (dInput.read() != -1) {}
		}
		return toHex(digest.digest());
	}

	private String toHex(byte[] bytes) {
		StringBuilder builder = new StringBuilder(bytes.length * 2);
		for (byte b : bytes) {
			builder.append(String.format("%02x", b));
			//builder.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
		}
		return builder.toString();
	}

}
```

## FileItem

ComputeFileChecksumVisitorで探索したファイル、ディレクトリの情報を格納するクラスです。
`path`フィールドは探索したファイル、ディレクトリの相対パス（起点パスからの）を格納します。
`checksum`フィールドは、探索したファイルのchecksumを、ディレクトリの場合は空文字を格納します。

### コード

```java:FileItem
import java.nio.file.Path;

public class FileItem implements Comparable<FileItem> {
	private Path path;
	private String checksum;

	public FileItem(Path path, String checksum) {
		this.path = path;
		this.checksum = checksum;
	}

	public Path getPath() {
		return path;
	}
	public String getChecksum() {
		return checksum;
	}

	@Override
	public int compareTo(FileItem o) {
		return this.compareTo(o);
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((checksum == null) ? 0 : checksum.hashCode());
		result = prime * result + ((path == null) ? 0 : path.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		FileItem other = (FileItem) obj;
		if (checksum == null) {
			if (other.checksum != null)
				return false;
		} else if (!checksum.equals(other.checksum))
			return false;
		if (path == null) {
			if (other.path != null)
				return false;
		} else if (!path.equals(other.path))
			return false;
		return true;
	}

	@Override
	public String toString() {
		return "FileItem [path=" + path + ", checksum=" + checksum + "]";
	}

}
```

# デモ

以下のように同じディレクトリ構成の`dir1`と`dir2`というディレクトリを作成しました。ファイル名もファイルの内容も同じですが、作成日時のタイムスタンプは異なります。
この`dir1`と`dir2`をデモコードでそれぞれ探索してファイル、ディレクトリの一覧とファイルのチェックサムを取得して同じ構造かどうかをチェックします。

```text
D:var
├─dir1
│  │  test1.txt
│  │  test2.txt
│  │
│  ├─aaa
│  │  └─ddd
│  ├─bbb
│  │      test3.txt
│  │
│  └─ccc
│          test4.txt
│
└─dir2
    │  test1.txt
    │  test2.txt
    │
    ├─aaa
    │  └─ddd
    ├─bbb
    │      test3.txt
    │
    └─ccc
            test4.txt
```

実行する

```java:Demo
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

public class Demo {

	public static void main(String[] args) throws Exception {

		Path dir1 = Paths.get("D:", "var", "dir1");
		ComputeFileChecksumVisitor dir1Visit = new ComputeFileChecksumVisitor(dir1, "MD5");
		Files.walkFileTree(dir1, dir1Visit);

		Path dir2 = Paths.get("D:", "var", "dir2");
		ComputeFileChecksumVisitor dir2Visit = new ComputeFileChecksumVisitor(dir2, "MD5");
		Files.walkFileTree(dir2, dir2Visit);

		List<FileItem> dir1Files = dir1Visit.getResult();
		System.out.println("Root : " + dir1.toString());
		dir1Files.forEach(System.out::println);

		List<FileItem> dir2Files = dir2Visit.getResult();
		System.out.println("Root : " + dir2.toString());
		dir2Files.forEach(System.out::println);

		if (dir1Files.equals(dir2Files)) {
			System.out.println("equal");
		} else {
			System.out.println("not equal");
		}
	}

}
```

実行した結果

```text
Root : D:\var\dir1
FileItem [path=aaa, hash=]
FileItem [path=aaa\ddd, hash=]
FileItem [path=bbb, hash=]
FileItem [path=bbb\test3.txt, hash=ed6e956a3d549303751e3238ab04bb46]
FileItem [path=ccc, hash=]
FileItem [path=ccc\test4.txt, hash=2c97af7af48689fc67a2700d9f051af6]
FileItem [path=test1.txt, hash=ac6a2aaa9317ef1f007c092c6a5fd75e]
FileItem [path=test2.txt, hash=811ad90a8dafc585bb64b23b6200969e]
Root : D:\var\dir2
FileItem [path=aaa, hash=]
FileItem [path=aaa\ddd, hash=]
FileItem [path=bbb, hash=]
FileItem [path=bbb\test3.txt, hash=ed6e956a3d549303751e3238ab04bb46]
FileItem [path=ccc, hash=]
FileItem [path=ccc\test4.txt, hash=2c97af7af48689fc67a2700d9f051af6]
FileItem [path=test1.txt, hash=ac6a2aaa9317ef1f007c092c6a5fd75e]
FileItem [path=test2.txt, hash=811ad90a8dafc585bb64b23b6200969e]
equal
```