FileVisitor でツリーっぽい表示

# きっかけ

> [Javaを使ってディレクトリの階層構造をツリー形式で表示する](http://qiita.com/ukiuki/items/1c70a7f907c5ac12b39f)
by @ukiuki 

を見て、[FileVisitor](http://docs.oracle.com/javase/8/docs/api/java/nio/file/FileVisitor.html) でなんとかならんかなーと思ったのがきっかけ。


結論から言うと、なんとかなったけどあまり綺麗にならない…
綺麗にするのはあきらめたけどここで供養

(File/Directory に visitor パターンはしっくりこないことが多いのでそもそもトライしてません)


## コード
jdk1.8.0 ＆ pleiades neon で開発。

### 自前 walk 処理版
```text:出力例
C:\Users\heignamerican\testroom\filewalk
├─dir  0\
│  ├─123.txt
│  ├─456.txt
│  └─dir01\
│      └─dir02\
├─dir1\
│  ├─dir1-1\
│  │  └─file1-1-1.txt
│  └─file1.txt
├─dir2\
│  ├─file  2.txt
│  ├─file1.txt
│  └─ほげほげ.txt
├─dir3\
├─dir4\
│  └─dir1-1 - ショートカット.lnk
└─file0.txt

C:\Users\heignamerican\testroom\filewalk\file0.txt

C:\Users\heignamerican\testroom\filewalk\dir4\dir1-1 - ショートカット.lnk

Exception in thread "main" java.lang.NullPointerException
	at java.util.Objects.requireNonNull(Unknown Source)
	at heignamerican.files.Walking$MyFileWalker.start(Walking.java:42)
	at heignamerican.files.Walking.main(Walking.java:17)

```

```Walking.java
package heignamerican.files;

import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

public class Walking {
    public static void main(final String[] args) throws IOException {
        new MyFileWalker(System.out).start(Paths.get("C:/Users/heignamerican/testroom/filewalk"));
        new MyFileWalker(System.out).start(Paths.get("C:/Users/heignamerican/testroom/filewalk/file0.txt"));
        new MyFileWalker(System.out).start(Paths.get("C:/Users/heignamerican/testroom/filewalk/dir4/dir1-1 - ショートカット.lnk"));
        new MyFileWalker(System.out).start(null);
    }

    public static class MyFileWalker {
        public MyFileWalker(final PrintStream out) {
            this.out = out;
        }

        private final PrintStream out;
        private StringBuilder indenter;

        /**
         * <p>
         * 指定された root 以下を tree っぽく表示する。
         *
         * <p>
         * コンストラクタで指定された {@code PrintStream} に出力する。<br>
         * SymbolicLink はたどらない(通常のファイル扱い)。<br>
         * スレッドセーフではない。<br>
         *
         * @param root
         *            null 禁止
         * @throws IOException
         */
        public void start(final Path root) throws IOException {
            Objects.requireNonNull(root);
            if (!Files.exists(root)) {
                out.printf("'%s' not found.%n", root);
                return;
            }
            out.println(root.toString());
            if (Files.isDirectory(root)) {
                indenter = new StringBuilder();
                walk(root);
            }
        }

        private static final String FILE_SEPARATOR = System.getProperty("file.separator");
        private static final String PREFIX_FOR_MIDDLE = "├─";
        private static final String PREFIX_FOR_LAST = "└─";
        private static final String INDENT_FOR_MIDDLE = "│  ";
        private static final String INDENT_FOR_LAST = "    ";

        private void walk(final Path path) throws IOException {
            final List<Path> children = Files.list(path).collect(Collectors.toList());

            final int lastIndex = children.size() - 1;
            for (int index = 0; index <= lastIndex; index++) {
                final Path child = children.get(index);
                final boolean isDirectory = Files.isDirectory(child);
                final boolean isMiddle = index != lastIndex;

                out.println(indenter.toString() + (isMiddle ? PREFIX_FOR_MIDDLE : PREFIX_FOR_LAST) + child.getFileName() + (isDirectory ? FILE_SEPARATOR : ""));

                if (isDirectory) {
                    final int oldLength = indenter.length();
                    indenter.append(isMiddle ? INDENT_FOR_MIDDLE : INDENT_FOR_LAST);
                    walk(child);
                    // if (notLast) { out.println(indenter); } // cmd の "tree /F" っぽくするなら入れたい
                    indenter.setLength(oldLength);
                }
            }
        }
    }
}
```

### SimpleFileVisitor 版


```text:出力例
C:\Users\heignamerican\testroom\filewalk
├─dir  0\
│　├─123.txt
│　├─456.txt
│　└─dir01\
│　　　└─dir02\
├─dir1\
│　├─dir1-1\
│　│　└─file1-1-1.txt
│　└─file1.txt
├─dir2\
│　├─file  2.txt
│　├─file1.txt
│　└─ほげほげ.txt
├─dir3\
├─dir4\
│　└─dir1-1 - ショートカット.lnk
└─file0.txt

C:\Users\heignamerican\testroom\filewalk\file0.txt

C:\Users\heignamerican\testroom\filewalk\dir4\dir1-1 - ショートカット.lnk

Exception in thread "main" java.lang.NullPointerException
	at java.util.Objects.requireNonNull(Unknown Source)
	at heignamerican.files.Walking2$MyFileVisitor.start(Walking2.java:43)
	at heignamerican.files.Walking2.main(Walking2.java:19)
```

```Walking2.java
package heignamerican.files;

import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Objects;
import java.util.Stack;

public class Walking2 {
	public static void main(final String[] args) throws IOException {
		new MyFileVisitor(System.out).start(Paths.get("C:/Users/heignamerican/testroom/filewalk"));
		new MyFileVisitor(System.out).start(Paths.get("C:/Users/heignamerican/testroom/filewalk/file0.txt"));
		new MyFileVisitor(System.out).start(Paths.get("C:/Users/heignamerican/testroom/filewalk/dir4/dir1-1 - ショートカット.lnk"));
		new MyFileVisitor(System.out).start(null);
	}

	public static class MyFileVisitor extends SimpleFileVisitor<Path> {
		public MyFileVisitor(final PrintStream out) {
			this.out = out;
		}

		private final PrintStream out;

		/**
		 * <p>
		 * 指定された root 以下を tree っぽく表示する。
		 *
		 * <p>
		 * コンストラクタで指定された {@code PrintStream} に出力する。<br>
		 * SymbolicLink はたどらない(通常のファイル扱い)。<br>
		 * スレッドセーフではない。<br>
		 *
		 * @param root
		 *            null 禁止
		 * @throws IOException
		 */
		public void start(final Path path) throws IOException {
			Objects.requireNonNull(path);
			if (!Files.exists(path)) {
				out.printf("'%s' not found.%n", path);
				return;
			}

			root = path;
			childrenCounts = new Stack<>();
			indenter = new StringBuilder();

			Files.walkFileTree(root, this);
		}

		private static final String FILE_SEPARATOR = System.getProperty("file.separator");
		private Path root;
		private Stack<Long> childrenCounts;
		private StringBuilder indenter;

		@Override
		public FileVisitResult visitFile(final Path path, final BasicFileAttributes attributes) throws IOException {
			if (path == root) {
				out.println(path);
			} else {
				out.println(indenter.toString() + getHereIndent() + path.getFileName());
				countdownChildren();
			}
			return FileVisitResult.CONTINUE;
		}

		@Override
		public FileVisitResult preVisitDirectory(final Path path, final BasicFileAttributes attributes) throws IOException {
			if (path == root) {
				out.println(path);
				listupChildrenCount(path);
			} else {
				out.println(indenter.toString() + getHereIndent() + path.getFileName() + FILE_SEPARATOR);
				increseIndent();
				listupChildrenCount(path);
			}
			return FileVisitResult.CONTINUE;
		}

		@Override
		public FileVisitResult postVisitDirectory(final Path path, final IOException attributes) throws IOException {
			if (path != root) {
				decreseIndent();
				countdownChildren();
			}
			return FileVisitResult.CONTINUE;
		}

		private void listupChildrenCount(final Path path) throws IOException {
			final long newer = Files.list(path).count();
			if (newer > 0) {
				childrenCounts.push(newer);
			}
		}

		private void countdownChildren() {
			final long rest = childrenCounts.pop() - 1;
			if (rest > 0) {
				childrenCounts.push(rest);
			}
		}

		private String getHereIndent() {
			return (childrenCounts.peek() > 1L) ? "├─" : "└─"; // インデントの文字数を 2 で固定する
		}

		private void increseIndent() {
			indenter.append((childrenCounts.peek() > 1L) ? "│　" : "　　"); // インデントの文字数を 2 で固定する
		}

		private void decreseIndent() {
			indenter.setLength(indenter.length() - 2); // インデントの文字数を 2 で固定する
		}
	}
}
```


## 反省
* ちょっと最後の方力尽きてます…
* 「最後**じゃない**」っていう英語表現が分からず、最後まで分からない変数名