書き方
qiita.rb
package your.package;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.*;
public class CsvReaderUtils {
public static <T> List<T> readCsv(Reader reader, Class<T> clazz) throws IOException {
List<T> result = new ArrayList<>();
try (BufferedReader br = new BufferedReader(reader)) {
String headerLine = readCsvLine(br);
if (headerLine == null || headerLine.trim().isEmpty()) {
throw new CsvValidationException("ヘッダーが存在しません。", 1, "ヘッダー");
}
String[] headers = parseCsvRow(headerLine);
Map<String, Field> headerFieldMap = buildHeaderFieldMap(clazz);
for (String header : headers) {
if (!headerFieldMap.containsKey(header.trim())) {
throw new CsvValidationException("ヘッダーがDTOと一致しません。", 1, header);
}
}
String line;
int lineNumber = 2;
while ((line = readCsvLine(br)) != null) {
String[] values = parseCsvRow(line);
T obj = mapToDto(clazz, headerFieldMap, headers, values, lineNumber);
result.add(obj);
lineNumber++;
}
} catch (CsvValidationException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("CSV読み込みエラー", e);
}
return result;
}
private static String readCsvLine(BufferedReader br) throws IOException {
StringBuilder sb = new StringBuilder();
String line;
boolean inQuotes = false;
while ((line = br.readLine()) != null) {
if (sb.length() > 0) sb.append("\n");
sb.append(line);
int quoteCount = countQuotes(sb.toString());
inQuotes = (quoteCount % 2) != 0;
if (!inQuotes) break;
}
return sb.length() == 0 ? null : sb.toString();
}
private static int countQuotes(String str) {
return (int) str.chars().filter(c -> c == '"').count();
}
private static String[] parseCsvRow(String line) {
List<String> result = new ArrayList<>();
boolean inQuotes = false;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (c == '"') {
if (inQuotes && i + 1 < line.length() && line.charAt(i + 1) == '"') {
sb.append('"');
i++;
} else {
inQuotes = !inQuotes;
}
} else if (c == ',' && !inQuotes) {
result.add(sb.toString().trim().replaceAll("\\s+$", ""));
sb.setLength(0);
} else {
sb.append(c);
}
}
result.add(sb.toString().trim().replaceAll("\\s+$", ""));
return result.toArray(new String[0]);
}
private static <T> Map<String, Field> buildHeaderFieldMap(Class<T> clazz) {
Map<String, Field> map = new HashMap<>();
for (Field field : clazz.getDeclaredFields()) {
CsvColumn anno = field.getAnnotation(CsvColumn.class);
String key = (anno != null ? anno.name() : field.getName()).trim();
field.setAccessible(true);
map.put(key, field);
}
return map;
}
private static <T> T mapToDto(Class<T> clazz, Map<String, Field> headerFieldMap,
String[] headers, String[] values, int lineNumber) throws Exception {
T obj = clazz.getDeclaredConstructor().newInstance();
for (int i = 0; i < headers.length; i++) {
String header = headers[i].trim();
Field field = headerFieldMap.get(header);
if (field == null) continue;
String rawValue = values[i].trim().replaceAll("\\s+$", "");
CsvValidation validation = field.getAnnotation(CsvValidation.class);
// === クォート必須チェック ===
boolean hasSurroundingQuotes = rawValue.startsWith("\"") && rawValue.endsWith("\"");
if (!hasSurroundingQuotes) {
throw new CsvValidationException("項目がダブルクォーテーションで囲まれていません。", lineNumber, header);
}
// === クォートを外して、内部 "" を " に戻す ===
String normalized = rawValue.substring(1, rawValue.length() - 1).replaceAll("\"\"", "\"");
// === 必須/長さ/パターンチェック ===
if (validation != null) {
if (validation.required() && normalized.isEmpty()) {
throw new CsvValidationException("必須項目が未入力です。", lineNumber, header);
}
CsvValidationException.checkLength(normalized, validation.minLength(), validation.maxLength(), lineNumber, header);
CsvValidationException.checkPattern(normalized, validation.pattern(), lineNumber, header);
}
// === 型変換 ===
Class<?> fieldType = field.getType();
try {
if (fieldType == int.class || fieldType == Integer.class) {
normalized = normalized.replaceFirst("^0+(?!$)", "");
field.set(obj, Integer.parseInt(normalized));
} else if (fieldType == long.class || fieldType == Long.class) {
normalized = normalized.replaceFirst("^0+(?!$)", "");
field.set(obj, Long.parseLong(normalized));
} else if (fieldType == double.class || fieldType == Double.class) {
normalized = normalized.replaceFirst("^0+(?!$)", "");
field.set(obj, Double.parseDouble(normalized));
} else if (fieldType == BigDecimal.class) {
normalized = normalized.replaceFirst("^0+(?!$)", "");
field.set(obj, new BigDecimal(normalized));
} else {
field.set(obj, normalized);
}
} catch (NumberFormatException e) {
throw new CsvValidationException("数値変換エラー: " + e.getMessage(), lineNumber, header);
}
}
return obj;
}
}
// CsvValidationException.java
package your.package;
/**
* CSVファイル読み込み時のバリデーションエラーを示すカスタム例外。
* 文字数や文字種のチェックにも対応。
*/
public class CsvValidationException extends RuntimeException {
/** エラーが発生したCSVファイルの行番号 */
private final int lineNumber;
/** エラーが発生したCSVファイルの項目名(ヘッダー名) */
private final String columnName;
/**
* 詳細メッセージ、行番号、項目名を指定して例外を作成する。
*
* @param message エラーの詳細な説明
* @param lineNumber エラーが発生した行番号
* @param columnName エラーが発生した項目名
*/
public CsvValidationException(String message, int lineNumber, String columnName) {
super(message);
this.lineNumber = lineNumber;
this.columnName = columnName;
}
/**
* エラーが発生した行番号を取得する。
*
* @return 行番号
*/
public int getLineNumber() {
return lineNumber;
}
/**
* エラーが発生した項目名を取得する。
*
* @return 項目名
*/
public String getColumnName() {
return columnName;
}
/**
* 詳細なエラー情報を含むメッセージを取得する。
*
* @return エラーメッセージ
*/
@Override
public String getMessage() {
return String.format("CSVバリデーションエラー [行番号: %d, 項目: '%s']: %s", lineNumber, columnName, super.getMessage());
}
/**
* 文字列が指定した長さの範囲内であるかをチェックする。
*
* @param value チェックする文字列
* @param minLength 最小文字数
* @param maxLength 最大文字数
* @param lineNumber チェック対象の行番号
* @param columnName チェック対象の項目名
*/
public static void checkLength(String value, int minLength, int maxLength, int lineNumber, String columnName) {
if (value.length() < minLength || value.length() > maxLength) {
throw new CsvValidationException("文字数が範囲外です(最小: " + minLength + "、最大: " + maxLength + ")", lineNumber, columnName);
}
}
/**
* 文字列が指定した正規表現パターンに一致するかをチェックする。
*
* @param value チェックする文字列
* @param pattern 正規表現パターン
* @param lineNumber チェック対象の行番号
* @param columnName チェック対象の項目名
*/
public static void checkPattern(String value, String pattern, int lineNumber, String columnName) {
if (!value.matches(pattern)) {
throw new CsvValidationException("指定された文字種以外が含まれています。", lineNumber, columnName);
}
}
}
package your.package;
import java.lang.annotation.*;
/**
* CSVファイルのヘッダー名を指定するためのアノテーション。
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface CsvColumn {
/** CSVファイルのヘッダー名 */
String name();
}
package your.package;
import java.lang.annotation.*;
/**
* CSV項目のバリデーションを指定するためのアノテーション。
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface CsvValidation {
/** 最小文字数 */
int minLength() default 0;
/** 最大文字数 */
int maxLength() default Integer.MAX_VALUE;
/** 文字種を制限する正規表現 */
String pattern() default ".*";
}
package your.package;
/**
* CSVファイルから読み込まれる個人情報のDTOクラス。
*/
public class PersonDto {
/** 氏名 */
@CsvColumn(name = "氏名")
@CsvValidation(minLength = 1, maxLength = 50, pattern = "^[ぁ-んァ-ヶ一-龥]+$")
private String name;
/** 年齢 */
@CsvColumn(name = "年齢")
private int age;
/** 在住地 */
@CsvColumn(name = "在住地")
@CsvValidation(maxLength = 100)
private String address;
// getter と setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
}
package your.package;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Field;
import java.util.*;
/**
* CSVファイルを読み込み、指定されたDTOクラスにマッピングするユーティリティクラス。
*/
public class CsvReaderUtils {
/**
* CSVファイルを読み込みDTOのリストに変換する。
*
* @param reader CSVファイルのリーダー
* @param clazz DTOクラス
* @param <T> DTOの型
* @return DTOのリスト
* @throws IOException ファイル読み込みエラー
*/
public static <T> List<T> readCsv(Reader reader, Class<T> clazz) throws IOException {
List<T> result = new ArrayList<>();
try (BufferedReader br = new BufferedReader(reader)) {
String headerLine = readCsvLine(br);
if (headerLine == null || headerLine.trim().isEmpty()) {
throw new CsvValidationException("ヘッダーが存在しません。", 1, "ヘッダー");
}
String[] headers = parseCsvRow(headerLine);
Map<String, Field> headerFieldMap = buildHeaderFieldMap(clazz);
for (String header : headers) {
if (!headerFieldMap.containsKey(header.trim())) {
throw new CsvValidationException("ヘッダーがDTOと一致しません。", 1, header);
}
}
String line;
int lineNumber = 2;
while ((line = readCsvLine(br)) != null) {
String[] values = parseCsvRow(line);
T obj = mapToDto(clazz, headerFieldMap, headers, values, lineNumber);
result.add(obj);
lineNumber++;
}
} catch (CsvValidationException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("CSV読み込みエラー", e);
}
return result;
}
/** CSVの1行を読み取る。 */
private static String readCsvLine(BufferedReader br) throws IOException {
StringBuilder sb = new StringBuilder();
String line;
boolean inQuotes = false;
while ((line = br.readLine()) != null) {
if (sb.length() > 0) sb.append("\n");
sb.append(line);
int quoteCount = countQuotes(sb.toString());
inQuotes = (quoteCount % 2) != 0;
if (!inQuotes) break;
}
return sb.length() == 0 ? null : sb.toString();
}
/** クォートの数を数える。 */
private static int countQuotes(String str) {
return (int) str.chars().filter(c -> c == '"').count();
}
/** CSVの1行を各項目に分割する。 */
private static String[] parseCsvRow(String line) {
List<String> result = new ArrayList<>();
boolean inQuotes = false;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (c == '"') {
if (inQuotes && i + 1 < line.length() && line.charAt(i + 1) == '"') {
sb.append('"');
i++;
} else {
inQuotes = !inQuotes;
}
} else if (c == ',' && !inQuotes) {
result.add(sb.toString().trim().replaceAll("\\s+$", ""));
sb.setLength(0);
} else {
sb.append(c);
}
}
result.add(sb.toString().trim().replaceAll("\\s+$", ""));
return result.toArray(new String[0]);
}
/** ヘッダーとフィールドのマッピングを作成する。 */
private static <T> Map<String, Field> buildHeaderFieldMap(Class<T> clazz) {
Map<String, Field> map = new HashMap<>();
for (Field field : clazz.getDeclaredFields()) {
CsvColumn anno = field.getAnnotation(CsvColumn.class);
String key = (anno != null ? anno.name() : field.getName()).trim();
field.setAccessible(true);
map.put(key, field);
}
return map;
}
/** CSVデータをDTOにマッピングし、バリデーションを実施する。 */
private static <T> T mapToDto(Class<T> clazz, Map<String, Field> headerFieldMap,
String[] headers, String[] values, int lineNumber) throws Exception {
T obj = clazz.getDeclaredConstructor().newInstance();
for (int i = 0; i < headers.length; i++) {
String header = headers[i].trim();
Field field = headerFieldMap.get(header);
if (field == null) continue;
String rawValue = values[i].trim().replaceAll("\\s+$", "");
CsvValidation validation = field.getAnnotation(CsvValidation.class);
if (validation != null) {
CsvValidationException.checkLength(rawValue, validation.minLength(), validation.maxLength(), lineNumber, header);
CsvValidationException.checkPattern(rawValue, validation.pattern(), lineNumber, header);
}
if (field.getType() == int.class || field.getType() == Integer.class) {
rawValue = rawValue.replaceFirst("^0+(?!$)", "");
field.set(obj, Integer.parseInt(rawValue));
} else {
field.set(obj, rawValue);
}
}
return obj;
}
}