ディレクトリのリスト表示ページをSpring Bootで作成する

### はじめに
この記事は以前執筆した [bedrock-vizで統合版Minecraftの地図を作る
](https://qiita.com/kabegamikamio/items/2a1aabf2c09974d1743a) という記事内で実装したスクリプトを発展させる目的で編集しています。
Bedrock-vizというMinecraftのマップツールと併用することを目的としていますが、用いている技術自体は別の用途にも十分応用可能です。

### 作ったサイト
[こちら](https://ourcity-map.localto.net/map-top) で確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/626368/e48cd281-a7b9-4604-4c91-42756b7f1248.png)

### 要件
- Spring Boot 3.3.5
    - Java: Oracle OpenJDK 23.0.1
    - (Bootstrap 4.3.1)
- サーバ
    - Ubuntu Desktop 24.04.1 LTS
    - Apache HTTP Server: 2.4.58

### 忙しい人のために
#### 概観
Controllerは `/map-top` のリクエストがあると、マップのディレクトリのリストを作成し、必要に応じて昇順・降順に並び替えます。
ソート済みディレクトリリストを `ModelAndView` に格納して、Viewに渡します。

ViewはControllerから受け取ったディレクトリリストからディレクトリ名を一つずつ取り出し、各マップページに行くためのハイパーリンク (e.g., `http://ourcity-map.localto.net/map/2024-10`) を併記して表示します。
この部分の処理にはThymeleafの `th:each` を使っています。

#### 実装
`MapListController.java` と `index.html` (マップサイトのトップページ用テンプレート) の実装を示します。
ここで特記していないファイルについては、spring initializrで生成したままと解釈してください。

```java:MapListController.java
package bedrock.map.maplist.maplist;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import java.io.File;
import java.util.Arrays;
import java.util.Collections;

@Controller
public class MapListController {
    /**
     * マップのリストを作成してSpringMVCでWeb公開するためのツールのコアController
     * @param mv ModelAndView
     * @return ModelAndView
     */
    @RequestMapping("/map-top")
    public ModelAndView index(ModelAndView mv, @RequestParam(name= "ascending", required = false) boolean ascending) {
        String path = "html/templates/map";
        File dir = new File(path);
        File[] files = dir.listFiles();
        int length;

        // 昇順 or 降順にソート
        if (files != null) {
            Arrays.sort(files);
            if (!ascending) {
                Arrays.sort(files, Collections.reverseOrder());
            }

            // ディレクトリ内のファイル名を格納するarrayのサイズ
            length = files.length;
        } else {
            length = 0;    // arrayが空ならサイズは0
        }

        String[] fileList = new String[length];
        String[] fileDirList = new String[length];

        // ディレクトリ内にあるファイルの名前をひとつずつ取得
        for (int i = 0; i < length; i++) {
            File file = files[i];
            fileList[i] = file.getName();
        }

        mv.setViewName("index");
        mv.addObject("fileList", fileList);
        mv.addObject("orderSwitch", ascending ? "降順にする" : "昇順にする");
        mv.addObject("orderSwitchFlag", !ascending);
        return mv;
    }
}
```

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Map List by Spring Boot</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <header>
        <div class="navbar navbar-dark bg-dark shadow-sm">
            <a href="#" class="navbar-brand d-flex align-items-center"><strong>OurCity Map Project</strong></a>
        </div>
    </header>
    <section class="jumbotron text-center">
        <div class="container">
            <h1 class="display-4 mb-4">OurCity Mapへようこそ</h1>
            <img th:src="@{/img/map_banner.png}" alt="map banner" height="400">
            <p class="lead text-muted">
                OurCity Map Projectは、OurCityの変遷を手軽に感じ取ることができる枠組みを提供することを目的とした活動です。<br>
                有志によってサーバ運営、Web開発を行っています。<br>
                <strong>地図は毎月1日更新です!!</strong>
            </p>
        </div>
    </section>
    <div class="container">
        <h1 class="display-4 mb-4">Map List</h1>
        <p class="alert-primary alert">地図が真っ白で何も表示されない場合は、画面左の「E」ボタンを押すことで地図が見えます<br>
            (地図の初期位置がバグっているようです。原因不明なので対処出来ていません。)</p>
        <a class="btn btn-primary my-2" th:text="${orderSwitch}" th:href="@{/map-top(ascending=${orderSwitchFlag})}"></a>
        <ul th:each="file : ${fileList}">
          <li><a th:text="${file}" th:href="@{'http://ourcity-map.localto.net/map/' + ${file}}"></a></li>
        </ul>
    </div>
<footer class="text-muted">
    <div class="container">
        <p class="float-right">
            <a href="#">Back to Top</a>
        </p>
        <p>
            OurCity Map Project owned by kkamio.<br>
            その他の活動については、<a href="https://sites.google.com/view/imperial-metro-gov">帝都行政中心</a>から。
        </p>
    </div>
</footer>
</body>
</html>
```

またApacheの

#### ディレクトリ図
ここでは仮にApacheが外部に公開するディレクトリを `html/` とします。

```:HTTPサーバのディレクトリ
html/
|- templates/map/
    |- (各マップのHTMLが入ったディレクトリ)
```

```:Spring Bootのプロジェクトディレクトリ
|- src/main/
    |- java/bedrock/map/maplist/maplist/
        |- MapListApplication.java
        |- MapListController.java
    |- resources/
        |- application.properties
        |- static/img
            |- (画像)
        |- templates/
            |- index.html
```

```:httpd.conf
ProxyPass 
```

### As-Isの確認
- 作成した地図をアーカイブとして陳列する際、マップのディレクトリを参照してリストを自動的に生成している
    - 新しい地図が毎月自動生成されるので、リストの更新も自動で行いたいため
- リストの更新は **シェルスクリプト** を走らせて、HTMLを書かせている
    - Node.js などの技術に疎いため、暫定的にこのような実装になってしまった
    - これが理由でチープなwebページになってしまった
    - HTMLをシェルスクリプト内にベタ打ちしているため、拡張性もメンテナンス性も悪い

### To-Be
- 現在の機能は保ったまま、拡張性・メンテナンス性を高めたい
    - ソートの昇順・降順切り替えなどに対応させたい
    - ディレクトリを見れるなど、OS機能を使えるフレームワークを用いる必要性がある

### Spring Bootの選定理由
Spring Bootを選んだことには、特段の合理的な理由は特にありません。
最近少し勉強して馴染があり、Javaのパッケージを使えばOSの機能にもアクセスできるため、
今回求める機能は満足できそうだということでSpring Bootを選びました。
なんならマップを表示しているwebサイトで捌くリクエストの種類はそこまで多くないので、Spring Bootは少しオーバーな気もします。

### 実装における工夫
To-Beでも触れた通り、メンテナンス性や拡張性を損ねないように意識しています。
今回は副次的な機能としてソートの昇順・降順機能を追加しました。
Spring Bootではリクエストから値を取得できるのでこのようなことが簡単に実現できるんですね。

HTMLテンプレートについてはできるだけコンパクトになるように意識しています。
Thymeleafの `th:each` はこれを実現するために非常に有用でした。

### 困った点・課題点
- 元々Apacheが外部に公開するディレクトリに `map/` というディレクトリを作り、その中に各マップのデータを収めていたのですが、Spring Bootは外部パスを指定したとしても `resources/` というディレクトリ配下にあるHTMLしかViewとして扱えないようなので、ディレクトリ構造を改変する必要がありました。
- Apacheをうまく設定できなかった(知識不足)ため、本当は `https://ourcity-map.localto.net` へのリクエストでトップページを表示するようにしたかったのですが、ProxyPassの設定の都合上 `https://ourcity-map.localto.net/map-top` へのリクエストでトップページを出す設計にせざるを得ませんでした。
    - Spring Bootが出すテンプレート (ポート8080で公開) と従来通りApacheが捌くHTML (ポート80) を両立するためには、これらが別のディレクトリとなるようにする必要がありました。
    - Spring Boot内のTomcatの設定を頑張れば対処できそうな気もするので、今後修正していきたいと考えています
- 各マップページへの経路として、現状HTMLにURLをハードコードしている状態です。これも、例えばリバースプロキシサービスを乗り換えた際に手作業で修正する作業を伴うので、正直ナンセンスな実装だと思っています。
    - いずれはSpring Bootで捌けるように改修したいところです。