テキストファイル操作

テキストファイルの取込みと表示

フォームから.txtファイルをアップロードしてプロジェクト内に保存し、記載内容を表示する処理を実装します。

動作例です。
下記のようなテキストファイルをアップロードすると、src/main/resources/static/text/フォルダに保存されます。
また、src/main/resources/static/text/配下のフォルダのテキスト内容を一覧表示します。

   アップロード

機能を実装していきます。

Formクラス、DTO

Formクラス

ファイルアップロード時、Formクラスでファイルを受け取ります。
.txtの拡張子に限らず、ファイルはMultipartFileで受け取ります。
【/com/cmps/spring/form/TextOpeForm.java】

package com.cmps.spring.form;

import java.io.Serializable;
import org.springframework.web.multipart.MultipartFile;
import com.cmps.spring.validation.annotation.FileNotEmpty;
import com.cmps.spring.validation.annotation.FileSize;

import lombok.Data;

@Data
public class TextOpeForm implements Serializable {

	//テキストファイル
	@FileSize
	@FileNotEmpty
	@FileExtension
	private MultipartFile file;

}

ここでは、バリデーションを実行させる3つの自作アノテーションを用いています。(後ほど解説します。)
ファイルサイズを確認する@FileSize、ファイルが空でないことを確認する@FileNotEmpty、拡張子の正規表現チェックを行う@FileExtension

これらのバリデーションチェックはController内で実装することも可能ですが、オブジェクト指向に則ると、機能は分離されている方が望ましいので、アノテーションを作成します。

公式:MultipartFile (Spring Framework API) – Javadoc

DTOクラス

Viewで必要な内容を表示するために、加工した値を格納するDTOクラスを使用します。
ここではファイル名と、ファイルのテキスト内容をString型で保持します。
【/com/cmps/spring/dto/TextOpeDto.java】

package com.cmps.spring.dto;

import lombok.Data;

@Data
public class TextOpeDto {

	//ファイル名
	private String name;
	
	//ファイル内容
	private String content;
}

アノテーションの作成

@FileNotEmpty

@FileNotEmpty アノテーションにあたるインターフェースです。
【/com/cmps/spring/validation/annotation/FileNotEmpty.java】

package com.cmps.spring.validation.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.cmps.spring.validation.FileNotEmptyValidator;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

@Constraint(validatedBy = FileNotEmptyValidator.class)////このアノテーションを使用したときに実行されるクラスを指定
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface FileNotEmpty {
	String message() default "ファイルを添付してください。";////エラーメッセージ

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};
}

中身を見ても分かりづらいと思いますが、基本的に9割はフォーマット/定型句と考えて使用して問題ありません。
上記でコメントを入れた箇所のみ主に編集します。

@FileNotEmpty アノテーションの実装です。
【/com/cmps/spring/validation/FileNotEmptyValidator.java】

package com.cmps.spring.validation;

import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import com.cmps.spring.validation.annotation.FileNotEmpty;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class FileNotEmptyValidator implements ConstraintValidator<FileNotEmpty, MultipartFile> {

	@Override
	public void initialize(FileNotEmpty constraintAnnotation) {////
	}

	@Override
	public boolean isValid(MultipartFile file, ConstraintValidatorContext context) {
		// ファイルが空ではないかどうか または ファイル名が長さを持つか(存在するか) 判定
		return !file.isEmpty() || StringUtils.hasLength(file.getOriginalFilename());
	}
}

ここで具体的に実装しているのはisValid()メソッドのみです。
戻り値がtrueであればバリデーションをクリア、falseであればバリデーションに引っかかる」ように実装します。

以上で作成は完了です。アノテーション(インターフェース)をFormクラスに記載すれば、機能するようになります。

@FileSize

同様にして、@FileSize アノテーションについても見ていきます。
【/com/cmps/spring/validation/annotation/FileSize.java】

package com.cmps.spring.validation.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.cmps.spring.validation.FileSizeValidator;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

@Constraint(validatedBy = FileSizeValidator.class)////このアノテーションを使用したときに実行されるクラスを指定
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface FileSize {
	String message() default "ファイルサイズが無効です。";////エラーメッセージ

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};

	long max() default 500 * 1024; //デフォルトが500kB ////実装クラスで使用する変数のデフォルト値を設定
}

先ほどの@FileNotEmptyの場合と異なるのは、max()というパラメータを追加していることです。
これはFormクラスで変更可能なパラメータになります。

	//記載例
	@FileSize(max=300 * 1024) //// ファイルサイズ上限「300kB」で指定
	private MultipartFile file;

@FileSize アノテーションの実装です。
【/com/cmps/spring/validation/FileSizeValidator.java】

package com.cmps.spring.validation;

import org.springframework.web.multipart.MultipartFile;

import com.cmps.spring.validation.annotation.FileSize;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class FileSizeValidator implements ConstraintValidator<FileSize, MultipartFile> {

	private long max;

	@Override
	public void initialize(FileSize constraintAnnotation) {
		this.max = constraintAnnotation.max();////maxの値を取得
	}

	@Override
	public boolean isValid(MultipartFile file, ConstraintValidatorContext context) {
		return file.getSize() <= max;
	}
}

変数maxの値を取得している以外は、isValidメソッドの記述は非常にシンプルでしょう。

@FileExtension

@FileExtensionアノテーションです。
【/com/cmps/spring/validation/annotation/FileExtension.java】

package com.cmps.spring.validation.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.cmps.spring.validation.FileExtensionValidator;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

@Constraint(validatedBy = FileExtensionValidator.class)////実行クラスを指定
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface FileExtension {
	String message() default "無効な拡張子です。";////エラーメッセージ

	Class<?>[] groups() default {};

	Class<? extends Payload>[] payload() default {};

	String regExp() default "txt"; //デフォルトはtxt ////実装クラスで使用する変数のデフォルト値
}

先ほどの@FileSizeと同様に、アノテーション使用時に異なる値を設定することが可能です。

	//記載例
	@FileExtension(regExp = "(jpg|jpeg|png|gif|pdf)")////画像の拡張子
	private MultipartFile file;

@FileExtensionアノテーションの実装です。
【/com/cmps/spring/validation/FileExtensionValidator.java】

package com.cmps.spring.validation;

import java.util.regex.Pattern;
import org.springframework.web.multipart.MultipartFile;
import com.cmps.spring.validation.annotation.FileExtension;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class FileExtensionValidator implements ConstraintValidator<FileExtension, MultipartFile> {

	private String regExp;

	@Override
	public void initialize(FileExtension constraintAnnotation) {
		this.regExp = constraintAnnotation.regExp();
	}

	@Override
	public boolean isValid(MultipartFile file, ConstraintValidatorContext context) {
		// ファイル未添付の場合はバリデーションしない
		if (file == null || file.isEmpty()) return true;

		//正規表現パターンを作成(regExpがtxtなら、「○○.txt」)
		Pattern pattern = Pattern.compile(".\\." + regExp);

		//正規表現チェック
		return pattern.matcher(file.getOriginalFilename()).find();
	}
}

正規表現チェック自体は既知の学習内容で記述しています。
MultipartFileクラスのgetOriginalFilename()メソッドで、クライアント(ユーザー)送信時のファイル名の文字列型を取得しています。

参考:5.17. ファイルアップロード — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.0.2.RELEASE documentation

View

View

フォームからファイルをアップロード(input type=”file”)する場合には、「 enctype=”multipart/form-data” 」が必須になります。
【/src/main/resources/templates/textope/index.html】

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>テキストファイル操作</title>
	<link rel="stylesheet" th:href="@{/css/manual.css}">
</head>
<body>
	<h1>テキストファイル操作</h1>
	<div th:if="${resultMessage}" class="messageBlock"><!-- 完了メッセージ -->
		<p th:text="${resultMessage}" class="messageBlock_text"></p>
	</div>
	<div class="block">
		<h2>テキストファイルのアップロード</h2>
		<form method="post" th:action="@{/textope/upload}" enctype="multipart/form-data">
			<input type="file" name="file" accept="text/plain">
			<input type="submit" value="アップロード">
		</form>
		<th:block th:if="${textOpeForm != null}" th:object="${textOpeForm}"><!-- エラー表示 -->
			<p th:if="${#fields.hasErrors('file')}" th:errors="*{file}" style="color:red"></p>
		</th:block>
	</div>
	<div class="block">
		<h2>保存したテキストファイルの表示</h2>
		<table>
			<tr>
				<th>タイトル</th>
				<th>テキストファイルの内容</th>
			</tr>
			<tr th:each="file : ${list}" th:object="${file}">
				<td th:text="*{name}"></td>
				<td th:utext="*{content}"></td>
			</tr>
		</table>
	</div>
</body>
</html>

CSS

ある程度見た目を整えるCSSです。次の単元以降も使用しますので、上記HTMLに含まれないセレクタも含まれています。
【/src/main/resources/static/css/manual.css】

@charset "UTF-8";
th, td {
	border: 1px solid #666;
}
ul {
	padding-left: 0px;
}
h2 {
	margin: 0;
}
.block {
	border: 1px solid #BBB;
	padding: 10px;
	max-width: 800px;
}
.block:nth-child(n+2) {
	margin-top: 20px;
}
.result-block {
	margin-top: 30px;
	background-color: #EEFFFF;
	padding: 5px 10px;
	max-width: 600px;
}
.result-block td,
.result-block th {
	border: 1px solid #CCC;
	padding: 0px 20px;
}
.upload_list {
	display: flex;
	flex-wrap: wrap;
	gap: 20px;
}
.upload_item {
	border: 1px solid #ddd;
	list-style-type: none;
	padding: 5px;
	width: 150px;
}
.upload_item img {
	height: auto;
	width: 100%;
}

Controller

フォームの表示及びファイル情報をDTOクラスに格納して一覧表示するindexメソッドと、
フォームからアップロードされたファイルを受け取って保存するuploadメソッドを記載しています。
【/com/cmps/spring/controller/TextOpeController.java】

package com.cmps.spring.controller;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.cmps.spring.dto.TextOpeDto;
import com.cmps.spring.form.TextOpeForm;

@Controller
@RequestMapping("/textope")
public class TextOpeController {

	/**
	 * 初期画面表示
	 * @param model Model
	 * @param resultMessage String
	 * @return
	 */
	@GetMapping("")
	public String index(Model model,
			@ModelAttribute TextOpeForm textOpeForm,
			@ModelAttribute("resultMessage") String resultMessage) {

		//Viewに渡すListのインスタンス化
		//Viewに表示するために必要な情報を、TextOpeDtoオブジェクトに詰めて渡す
		List<TextOpeDto> list = new ArrayList<TextOpeDto>();

		//ディレクトリパスを指定してFileオブジェクトを生成
		File dir = new File("src/main/resources/static/text/");
		//listFilesメソッドを使用して一覧(Fileクラス型配列)を取得する
		File[] fileList = dir.listFiles();

                // TextOpeDtoオブジェクトに詰め直し→Listに格納する
                if (fileList.length > 0) {
                    try {
                        for (File file : fileList) {
                            // オブジェクト生成
                            TextOpeDto dto = new TextOpeDto();
                            // 名前を格納
                            dto.setName(file.getName());
        
                            // View側で改行が表示されるようにbrタグに変換
                            String content = Files.readString(file.toPath()).replaceAll("\r\n|\n", "<br>");
                            // DTOオブジェクトにcontentを格納
                            dto.setContent(content);
                            // リストに追加
                            list.add(dto);
                        }
        
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
		
		model.addAttribute("list", list);
		return "textope/index";
	}

	/**
	 * ファイルアップロード resources/static/textに保存する
	 * @param model Model
	 * @param redirectAttributes String
	 * @return
	 */
	@PostMapping("/upload")
	public String upload(Model model,
			@Validated TextOpeForm textOpeForm, BindingResult result,
			RedirectAttributes redirectAttributes) {

		//バリデーションエラー返却
		if (result.hasErrors()) {
			return index(model, textOpeForm, "");
		}

		//書き込み対象ファイルをPathとして取得
		//ディレクトリとファイル名から、フルパスを生成
		Path path = Path.of("src/main/resources/static/text/", textOpeForm.getFile().getOriginalFilename());

		try {
			//ファイル内容をコピー 第1引数InputStream→第2引数Path
			//第3引数StandardCopyOption.REPLACE_EXISTINGで、新規作成または同パスが存在していれば上書き
			Files.copy(textOpeForm.getFile().getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);

		} catch (IOException e) {
			e.printStackTrace();
			redirectAttributes.addFlashAttribute("resultMessage", "ファイルの保存に失敗しました。");
			return "redirect:/textope";
		}

		// redirectAttributesに登録
		redirectAttributes.addFlashAttribute("resultMessage", ".txtファイルのアップロードが完了しました。");
		return "redirect:/textope";
	}
}

indexメソッド

・indexメソッドでのViewに渡すListの作成の内容は、要約すると
①Fileクラスを用いて、ディレクトリパスからファイルの一覧を取得
②ファイルごとに取り出し、各種メソッドを組み合わせて必要な情報(ファイル名、テキスト内容)を取り出しDTOクラスに格納。
テキスト内容については、改行が有効に表示されるように改行コードをreplaceAllメソッドで置換しています。
③DTOをListに格納
④View表示

・上記では、Fileクラスから内容を取得するのに、FilesクラスのreadStringメソッドで取得しています。(引数としてはPathクラスに変換)
//View側で改行が表示されるようにbrタグに変換
String content = Files.readString(file.toPath()).replaceAll("\r\n|\n", "<br>");

FilesクラスはJava7で導入された比較的モダンなファイル操作APIです。Ver.が異なると使用できない場合があります。その場合の代替コードは以下です。
BufferedReaderも、javaでよく使用するファイル読み込みのためのクラスです。

//BufferedReaderで対象ファイルを読み込み
BufferedReader br = new BufferedReader(new FileReader(file));
//1行ずつ取得してcontentに書き込み
String line;
String content = "";
while ((line = br.readLine()) != null) {
	content += line;
}

公式:File (Java Platform SE 8 ) Files (Java Platform SE 8 )Path (Java Platform SE 8 )
参考サイト:
JavaのFileクラスとFilesクラスの特徴と違い
【Java】ファイル操作関連 #spring – Qiita ファイルやフォルダの作成・削除などの処理が実行可能

uploadメソッド

・ここでの主要な処理はFilesクラスのcopyメソッドです。
アップロードファイルをディレクトリ内のパスにコピー=保存されます。
Filesクラスが使用できない場合は、MultipartFileクラスのtransfetToメソッドでも実装可能です。
textOpeForm.getFile().transferTo(path);

・try~catchを多用しているのは、ファイル操作系のメソッドを使用する場合、検査例外IOException が発生するためです。

テキストファイルを用意しアップロードすると保存されブラウザに内容が表示されること、
および、各種バリデーションが機能することを確認しましょう。
(拡張子バリデーションの機能を確認するには、inputタグのaccept属性を一時的に消して試してみてください。 accept属性は添付ファイルのコンテントタイプを指定するHTMLの属性です。)

テキストファイルへの追記

アップロード済みのファイルに対して追記する処理を実装します。

今回はFormクラスを作成せずに簡易的に実装しているサンプルです。

View

View
【/src/main/resources/templates/textope/index.html】body閉じタグ前に追記

	<div class="block">
		<h2>テキストファイルの書き込み(追記)</h2>
		<p>sample.text に追記します。</p>
		<form method="post" th:action="@{/textope/append}">
			<textarea cols="45" rows="4" name="textPlus"></textarea>
			<input type="submit" value="送信">
		</form>
	</div>

textareaタグに記入した内容をフォーム送信します。

Controller

【/com/cmps/spring/controller/TextOpeController.java】追記

	/**
	 * 特定ファイルへの追記
	 * @param model Model
	 * @param redirectAttributes String
	 * @return
	 */
	@PostMapping("/append")
	public String appendFile(Model model,
			@RequestParam(required = false) String textPlus,
			RedirectAttributes redirectAttributes) {
		
		//簡易バリデーション(未入力チェック)
		if (textPlus.isBlank()) {
			// redirectAttributesに登録
			redirectAttributes.addFlashAttribute("resultMessage", "追記テキストが入力されていません。");
			return "redirect:/textope";
		}

		//書き込み対象ファイル
		Path path = Path.of("src/main/resources/static/text/sample.txt");
		
		try {
			//BufferedWriterによりStringをファイルに書き込み
			//第3引数StandardCopyOption.APPENDで、追記
			BufferedWriter bw = Files.newBufferedWriter(path, StandardCharsets.UTF_8, StandardOpenOption.APPEND);
			bw.write(textPlus + "\n");
			bw.close();

		} catch (IOException e) {
			e.printStackTrace();
			redirectAttributes.addFlashAttribute("resultMessage", "ファイルの書き込みに失敗しました。");
			return "redirect:/textope";
		}

		// redirectAttributesに登録
		redirectAttributes.addFlashAttribute("resultMessage", ".txtファイルの書き込みが完了しました。");
		return "redirect:/textope";
	}

textareaタグに記入した内容を受け取り(textPlus)、
書き込み対象Pathにsample.txtを指定して取得し、
BufferedWriterクラスのwriteメソッドに記入内容を渡して、追記を実行しています。¥nは改行コードです。

Files.newBufferedWriterメソッドの第3引数にStandardOpenOption.APPENDを指定することで、対象ファイルへ追記する準備をしています。

練習問題

問1: 新しいフォームを作成し、セレクトボックスで選択したファイルに対して追記を行う機能を実装してください。

練習問題のヒント
問1
①Viewを修正。
新しいフォームを作成し、セレクトボックスとテキストエリアを配置する。
セレクトボックスは表示名とvalueともにファイル名とする。

②フォームに対応するメソッドを作成する。
マニュアルのappendFileメソッドを元に、
・セレクトボックスのパラメータも受け取れるようにする。
・受け取ったファイル名を用いて、書き込み対象ファイルのPathを取得する。文字連結を用いると良い。

タイトルとURLをコピーしました