0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CSVダウンロードを作成してみた

Last updated at Posted at 2025-06-10

書き方

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;
    }
}



0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?