【改善編】簡単なツール作成を通してJavaを学ぶ

# お題
[前回](https://qiita.com/sky0621/items/cb14aa67ef327922c0a1)、Golangで書いた簡易ツール「ローカルJSONファイルに保存するキーバリューストア」のJava版。
シリーズ物としては、もともと複数言語で同じ内容のプログラムを書いて比較した「[簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ](https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff)」からの改善版。

# 試行Index
- 第１回：[簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ](https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff)
- 第２回：[【改善編】簡単なツール作成を通してRubyを学ぶ](https://qiita.com/sky0621/items/5868b19934187571d0cd)
- 第３回：[【改善編】簡単なツール作成を通してPython3を学ぶ](https://qiita.com/sky0621/items/27c610e08391c7cc83ea)
- 第４回：[【改善編】簡単なツール作成を通してGolangを学ぶ](https://qiita.com/sky0621/items/cb14aa67ef327922c0a1)
- 第５回：[【改善編】簡単なツール作成を通してJavaを学ぶ](https://qiita.com/sky0621/items/f19f5191754f61a4d4c3)
- 第６回：[【改善編】簡単なツール作成を通してScalaを学ぶ](https://qiita.com/sky0621/items/a68ae8b64d4856d1a195)
- 第７回：[簡単なツール作成を通してRustを学ぶ](https://qiita.com/sky0621/items/86d9159bc1930618162c)
- 第８回：[【改善編】簡単なツール作成を通してRustを学ぶ](https://qiita.com/sky0621/items/a70cc7a9453795e0c9e9)

# 実装・動作確認端末
### # 言語バージョン
```
$ java -version
openjdk version "1.8.0_202"
OpenJDK Runtime Environment (build 1.8.0_202-20190206132807.buildslave.jdk8u-src-tar--b08)
OpenJDK GraalVM CE 1.0.0-rc14 (build 25.202-b08-jvmci-0.56, mixed mode)
```

### # IDE - IntelliJ IDEA
```
IntelliJ IDEA 2019.2 (Ultimate Edition)
Build #IU-192.5728.98, built on July 23, 2019
```

# 実践

## 要件
アプリを起動すると、キーバリュー形式でテキスト情報をJSONファイルに保存する機能を持つコンソールアプリ。
オンメモリで保持していた点だけ除けば第１回と同じ仕様なので詳細は以下参照。
https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff#要件

### ソース全量
https://github.com/sky0621/book_java/tree/v0.2.0

## 解説

### 全ソースファイル

|Javaソース|説明|
|---|---|
|Main.java|アプリ起動エントリーポイント|
|StoreInfo.java|キーバリュー情報を保存するストア(JSONファイル)に関する情報を扱う。<br>現状は「ファイル名」だけ保持|
|Commands.java|キーバリューストアからの情報取得や保存、削除といった各コマンドを管理。<br>コマンドの増減に関する影響は、このソースに閉じる。|
|Command.java|各コマンドに共通のインタフェース|
|SaveCommand.java|キーバリュー情報の保存を担う。|
|GetCommand.java|指定キーに対するバリューの取得を担う。|
|ListCommand.java|全キーバリュー情報の取得を担う。|
|RemoveCommand.java|指定キーに対するバリューの削除を担う。|
|ClearCommand.java|全キーバリュー情報の削除を担う。|
|HelpCommand.java|ヘルプ情報の表示を担う。|
|EndCommand.java|アプリの終了を担う。|

### [Main.java]アプリ起動エントリーポイント

```java:[Main.java]
import java.util.Scanner;

public class Main {
    public static void main(String... args) {
        Commands commands = new Commands(new StoreInfo("store.json"));

        System.out.println("Start!");
        while (true) {
            Scanner s = new Scanner(System.in);
            String cmd = s.nextLine();
            commands.exec(cmd.split(" "));
        }
    }
}
```

### [StoreInfo.java]ストア情報の管理

```java:[StoreInfo.java]
import org.apache.commons.lang3.StringUtils;

public class StoreInfo {
    private static final String DEFAULT_STORE_NAME = "store.json";

    private String storeName;

    public StoreInfo(String storeName) {
        if (StringUtils.isEmpty(storeName)) {
            this.storeName = DEFAULT_STORE_NAME;
        } else {
            this.storeName = storeName;
        }
    }

    public String getName() {
        return this.storeName;
    }
}
```

### [Commands.java]各コマンドの管理

```java:[Commands.java]
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Commands {
    private Map<String, Command> commands;

    public Commands(StoreInfo storeInfo) {
        commands = new HashMap<>();
        commands.put("end", new EndCommand());
        commands.put("help", new HelpCommand());
        commands.put("clear", new ClearCommand(storeInfo));
        commands.put("save", new SaveCommand(storeInfo));
        commands.put("get", new GetCommand(storeInfo));
        commands.put("remove", new RemoveCommand(storeInfo));
        commands.put("list", new ListCommand(storeInfo));

        if (!new File(storeInfo.getName()).exists()) {
            commands.get("clear").exec(null);
        }
    }

    public void exec(String[] cmds) {
        if (cmds == null || cmds.length < 1) {
            System.out.println("no target");
            return;
        }

        List<String> cmdList = Arrays.asList(cmds);
        String[] args = cmdList.subList(1, cmdList.size()).toArray(new String[0]);
        this.commands.get(cmds[0]).exec(args);
    }
}
```

### [Command.java]各コマンドの親クラス

```java:[Command.java]
public interface Command {
    void exec(String[] args);
}
```

### 各コマンドクラス

各コマンドについては、ストア情報がオンメモリのハッシュからJSONに変わった点以外はやることは一緒。（なので説明省く。）

#### ■保存

```java:[SaveCommand.java]
import com.google.gson.Gson;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class SaveCommand implements Command {
    private StoreInfo storeInfo;

    public SaveCommand(StoreInfo storeInfo) {
        this.storeInfo = storeInfo;
    }

    @Override
    public void exec(String[] args) {
        if (args == null || args.length != 2) {
            System.out.println("not valid");
            return;
        }

        Gson gson = new Gson();
        Path p = Paths.get(this.storeInfo.getName());
        try {
            Map<String, String> now = gson.fromJson(Files.readAllLines(p).get(0), HashMap.class);
            now.put(args[0], args[1]);
            Files.write(p, gson.toJson(now).getBytes());
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}
```

#### ■１件取得

```java:[GetCommand.java]
import com.google.gson.Gson;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class GetCommand implements Command {
    private StoreInfo storeInfo;

    public GetCommand(StoreInfo storeInfo) {
        this.storeInfo = storeInfo;
    }

    @Override
    public void exec(String[] args) {
        if (args == null || args.length != 1) {
            System.out.println("not valid");
            return;
        }

        Path p = Paths.get(this.storeInfo.getName());
        try {
            Map<String, String> now = new Gson().fromJson(Files.readAllLines(p).get(0), HashMap.class);
            System.out.println(now.get(args[0]));
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}
```

#### ■全件取得

```java:[ListCommand.java]
import com.google.gson.Gson;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class ListCommand implements Command {
    private StoreInfo storeInfo;

    public ListCommand(StoreInfo storeInfo) {
        this.storeInfo = storeInfo;
    }

    @Override
    public void exec(String[] args) {
        Path p = Paths.get(this.storeInfo.getName());
        try {
            Map<String, String> now = new Gson().fromJson(Files.readAllLines(p).get(0), HashMap.class);
            System.out.println("\"key\",\"value\"");
            now.forEach((k, v) -> System.out.printf("\"%s\",\"%s\"\n", k, v));
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}
```

#### ■１件削除

```java:[RemoveCommand.java]
import com.google.gson.Gson;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class RemoveCommand implements Command {
    private StoreInfo storeInfo;

    public RemoveCommand(StoreInfo storeInfo) {
        this.storeInfo = storeInfo;
    }

    @Override
    public void exec(String[] args) {
        if (args == null || args.length != 1) {
            System.out.println("not valid");
            return;
        }

        Gson gson = new Gson();
        Path p = Paths.get(this.storeInfo.getName());
        try {
            Map<String, String> now = gson.fromJson(Files.readAllLines(p).get(0), HashMap.class);
            now.remove(args[0]);
            Files.write(p, gson.toJson(now).getBytes());
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}
```

#### ■全件削除

```java:[ClearCommand.java]
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ClearCommand implements Command {
    private StoreInfo storeInfo;

    public ClearCommand(StoreInfo storeInfo) {
        this.storeInfo = storeInfo;
    }

    @Override
    public void exec(String[] args) {
        try {
            Files.write(Paths.get(storeInfo.getName()), "{}".getBytes());
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
}
```

#### ■ヘルプ

```java:[HelpCommand.java]
public class HelpCommand implements Command {
    @Override
    public void exec(String[] args) {
        String msg = "\n" +
                "[usage]\n" +
                "キーバリュー形式で文字列情報を管理するコマンドです。\n" +
                "以下のサブコマンドが利用可能です。\n" +
                "\n" +
                "save   ... keyとvalueを渡して保存します。\n" +
                "get    ... keyを渡してvalueを表示します。\n" +
                "remove ... keyを渡してvalueを削除します。\n" +
                "list   ... 保存済みの内容を一覧表示します。\n" +
                "clear  ... 保存済みの内容を初期化します。\n" +
                "help   ... ヘルプ情報（当内容と同じ）を表示します。\n" +
                "end    ... アプリを終了します。\n" +
                "\n";
        System.out.println(msg);
    }
}
```

#### ■アプリ終了

```java:[EndCommand.java]
public class EndCommand implements Command {
    @Override
    public void exec(String[] args) {
        System.out.println("End!");
        System.exit(-1);
    }
}
```

# まとめ
だんだん雑になっていくのを自覚・・・。