java csv比較ツール

比較元と比較先のSJISエンコードされたCSVファイルをSQLiteデータベースにインポートし、その差分をSJISエンコードされたCSVファイルとして出力するSwingベースのアプリケーションです。


```java
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.border.LineBorder;

import org.apache.commons.lang3.StringUtils;
import org.supercsv.io.CsvListWriter;
import org.supercsv.prefs.CsvPreference;
import org.supercsv.quote.AlwaysQuoteMode;
import org.supercsv.quote.NormalQuoteMode;

import com.opencsv.CSVReader;
import com.opencsv.CSVReaderBuilder;
import com.opencsv.exceptions.CsvValidationException;

public class CsvToSQLiteApp extends JFrame {
	private static final String DB_URL = "jdbc:sqlite:example.db";
	private static final int BATCH_SIZE = 1000;
	private static final int CHUNK_SIZE = 10000; // 1万件ごとに分割するための定数
	private JTextField beforeDirPathField;
	private JTextField afterDirPathField;
	private JTextField outputCsvPathField;
	private JCheckBox headerCheckBox;
	private JCheckBox outPutHeaderCheckBox;
	private JLabel statusLabel;
	private JTextArea messageArea;
	private JProgressBar progressBar;
	private JPanel columnSelectionPanel;
	private JButton importButton;
	private List<JCheckBox> columnCheckBoxes = new ArrayList<>();
	// 出力の囲い文字設定用のフィールドを追加
	private JTextField outputQuoteField;
	// 出力ファイル名設定用のフィールドを追加
	private JTextField outputFileNameField;
	private CsvPreference preference = new CsvPreference.Builder('\"', ',', "\r\n")
			.useQuoteMode(new AlwaysQuoteMode())
			.build();

	public CsvToSQLiteApp() {
		setTitle("CSV to SQLite App");
		setBounds(0, 100, 620, 780);
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setLayout(new FlowLayout());
		setResizable(false);
		GridBagLayout layout = new GridBagLayout();
		GridBagConstraints gbc = new GridBagConstraints();

		//比較元ディレクトリパス の設定
		JLabel beforeDirPathLabel = new JLabel("比較元ディレクトリパス : ");
		beforeDirPathField = new JTextField(35);
		JButton selectBeforeDirButton = new JButton("選択");
		selectBeforeDirButton.setPreferredSize(new Dimension(60, 20));
		JPanel p = new JPanel();
		p.setLayout(layout);
		p.setPreferredSize(new Dimension(600, 40));
		p.setLayout(new FlowLayout(FlowLayout.LEFT));
		p.setBorder(new LineBorder(new Color(220, 220, 220), 2, true));

		gbc.gridx = 0;
		gbc.gridy = 0;
		p.add(beforeDirPathLabel, gbc);

		gbc.gridx = 1;
		p.add(beforeDirPathField, gbc);

		gbc.gridx = 2;
		p.add(selectBeforeDirButton, gbc);
		getContentPane().add(p, BorderLayout.PAGE_START);

		JLabel afterDirPathLabel = new JLabel("比較先ディレクトリパス : ");
		afterDirPathField = new JTextField(35);
		JButton selectAfterDirButton = new JButton("選択");
		selectAfterDirButton.setPreferredSize(new Dimension(60, 20));

		JPanel p2 = new JPanel();
		p2.setLayout(layout);

		p2.setPreferredSize(new Dimension(600, 40));
		p2.setLayout(new FlowLayout(FlowLayout.LEFT));
		p2.setBorder(new LineBorder(new Color(220, 220, 220), 2, true));

		gbc.gridx = 0;
		gbc.gridy = 0;
		p2.add(afterDirPathLabel, gbc);

		gbc.gridx = 1;
		p2.add(afterDirPathField, gbc);

		gbc.gridx = 2;
		p2.add(selectAfterDirButton, gbc);
		getContentPane().add(p2, BorderLayout.PAGE_START);

		headerCheckBox = new JCheckBox("取込ファイルの1行目をヘッダー行とする");
		headerCheckBox.setSelected(true);
		JPanel checkP = new JPanel();
		checkP.setLayout(layout);
		checkP.setPreferredSize(new Dimension(600, 40));
		checkP.setLayout(new FlowLayout(FlowLayout.LEFT));
		checkP.add(headerCheckBox, gbc);
		getContentPane().add(checkP, BorderLayout.PAGE_START);

		JLabel outputCsvPathLabel = new JLabel("出力先ディレクトリパス : ");
		outputCsvPathField = new JTextField(35);
		JButton diffButton = new JButton("選択");
		diffButton.setPreferredSize(new Dimension(60, 20));

		JPanel p3 = new JPanel();
		p3.setLayout(layout);

		p3.setPreferredSize(new Dimension(600, 40));
		p3.setLayout(new FlowLayout(FlowLayout.LEFT));
		p3.setBorder(new LineBorder(new Color(220, 220, 220), 2, true));

		gbc.gridx = 0;
		gbc.gridy = 0;
		p3.add(outputCsvPathLabel, gbc);

		gbc.gridx = 1;
		p3.add(outputCsvPathField, gbc);

		gbc.gridx = 2;
		p3.add(diffButton, gbc);
		getContentPane().add(p3, BorderLayout.PAGE_START);

		// 出力の囲い文字設定用のコンポーネントを追加
		JLabel outputQuoteLabel = new JLabel("出力の囲い文字:");
		outputQuoteField = new JTextField(10);
		outputQuoteField.setPreferredSize(new Dimension(20, 20));
		outputQuoteField.setText("\""); // デフォルト値を設定
		JPanel p4 = new JPanel();

		p4.setPreferredSize(new Dimension(298, 40));
		p4.setLayout(new FlowLayout(FlowLayout.LEFT));
		p4.setBorder(new LineBorder(new Color(220, 220, 220), 2, true));
		p4.add(outputQuoteLabel, gbc);
		p4.add(outputQuoteField, gbc);
		getContentPane().add(p4, BorderLayout.PAGE_START);

		// 出力ファイル名設定用のコンポーネントを追加
		JLabel outputFileNameLabel = new JLabel("出力ファイル名:");
		outputFileNameField = new JTextField(15);
		outputFileNameField.setPreferredSize(new Dimension(20, 20));
		outputFileNameField.setText("output");
		JPanel p5 = new JPanel();

		p5.setPreferredSize(new Dimension(297, 40));
		p5.setLayout(new FlowLayout(FlowLayout.LEFT));
		p5.setBorder(new LineBorder(new Color(220, 220, 220), 2, true));
		p5.add(outputFileNameLabel, gbc);
		p5.add(outputFileNameField, gbc);
		getContentPane().add(p5, BorderLayout.PAGE_START);

		outPutHeaderCheckBox = new JCheckBox("出力ファイルにヘッダーを出力する");
		outPutHeaderCheckBox.setSelected(true);
		JPanel checkP2 = new JPanel();
		checkP2.setLayout(layout);
		checkP2.setPreferredSize(new Dimension(600, 40));
		checkP2.setLayout(new FlowLayout(FlowLayout.LEFT));
		checkP2.add(outPutHeaderCheckBox, gbc);
		getContentPane().add(checkP2, BorderLayout.PAGE_START);

		statusLabel = new JLabel("進捗状況:");
		progressBar = new JProgressBar();
		progressBar.setStringPainted(true);
		progressBar.setPreferredSize(new Dimension(580, 20));
		JPanel barP = new JPanel();
		barP.setPreferredSize(new Dimension(600, 50));
		barP.setLayout(new FlowLayout(FlowLayout.LEFT));
		barP.add(statusLabel, gbc);
		barP.add(progressBar, gbc);
		getContentPane().add(barP, BorderLayout.PAGE_START);

		JPanel msgP = new JPanel();
		msgP.setPreferredSize(new Dimension(600, 200));
		msgP.setLayout(new FlowLayout(FlowLayout.LEFT));
		messageArea = new JTextArea(580, 50);
		messageArea.setEditable(false);
		JLabel msgLabel = new JLabel("メッセージ:");
		JScrollPane messageScrollPane = new JScrollPane(messageArea);
		messageScrollPane.setPreferredSize(new Dimension(580, 150));
		msgP.add(msgLabel, gbc);
		msgP.add(messageScrollPane, gbc);
		getContentPane().add(msgP, BorderLayout.PAGE_START);

		importButton = new JButton("実行");
		JPanel inputBtnP = new JPanel();
		inputBtnP.setLayout(new GridBagLayout());
		inputBtnP.setPreferredSize(new Dimension(600, 40));
		inputBtnP.setLayout(new FlowLayout(FlowLayout.RIGHT));
		inputBtnP.add(importButton, gbc);
		getContentPane().add(inputBtnP, BorderLayout.PAGE_START);

		selectBeforeDirButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				JFileChooser fileChooser = new JFileChooser();
				fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
				int result = fileChooser.showOpenDialog(null);
				if (result == JFileChooser.APPROVE_OPTION) {
					File selectedDir = fileChooser.getSelectedFile();
					beforeDirPathField.setText(selectedDir.getAbsolutePath());
				}
			}
		});

		selectAfterDirButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				JFileChooser fileChooser = new JFileChooser();
				fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
				int result = fileChooser.showOpenDialog(null);
				if (result == JFileChooser.APPROVE_OPTION) {
					File selectedDir = fileChooser.getSelectedFile();
					afterDirPathField.setText(selectedDir.getAbsolutePath());
				}
			}
		});

		diffButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				JFileChooser fileChooser = new JFileChooser();
				fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
				int result = fileChooser.showOpenDialog(null);
				if (result == JFileChooser.APPROVE_OPTION) {
					File selectedDir = fileChooser.getSelectedFile();
					outputCsvPathField.setText(selectedDir.getAbsolutePath());
				}
			}
		});

		importButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				String beforeDirPath = beforeDirPathField.getText();
				String afterDirPath = afterDirPathField.getText();
				String outputCsvPath = outputCsvPathField.getText();
				boolean hasHeader = headerCheckBox.isSelected();
				String outputQuote = outputQuoteField.getText();
				String outputFileName = outputFileNameField.getText();

				if (StringUtils.isNotEmpty(outputQuote)) {
					preference = new CsvPreference.Builder(outputQuote.charAt(0), ',', "\r\n")
							.useQuoteMode(new AlwaysQuoteMode())
							.build();
				} else {
					preference = new CsvPreference.Builder('\"', ',', "\r\n")
							.useQuoteMode(new NormalQuoteMode())
							.build();
				}

				SwingWorker<Void, Integer> worker = new SwingWorker<Void, Integer>() {
					@Override
					protected Void doInBackground() throws Exception {
						updateProgress(0);

						// データベースファイルの削除
						File dbFile = new File("example.db");
						if (dbFile.exists()) {
							dbFile.delete();
						}

						try (Connection conn = DriverManager.getConnection(DB_URL)) {
							importCsvToTable(conn, beforeDirPath, "before", hasHeader);
							publish(50);
							importCsvToTable(conn, afterDirPath, "after", hasHeader);
							publish(66);
							createIndexes(conn, "before");
							createIndexes(conn, "after");
							exportDiffToCsv(conn, "before", "after", outputCsvPath, outputFileName);
							exportDiffDelToCsv(conn, "after", "before", outputCsvPath, outputFileName);
							publish(100);

							// カラムリストの更新
							updateColumnSelectionPanel(conn);
						} catch (SQLException ex) {
							ex.printStackTrace();
							appendMessage("CSVファイルの処理中にエラーが発生しました: " + ex.getMessage());
							JOptionPane.showMessageDialog(null, "CSVファイルの処理中にエラーが発生しました: " + ex.getMessage());
						}

						return null;
					}

					@Override
					protected void process(List<Integer> chunks) {
						int latestProgress = chunks.get(chunks.size() - 1);
						updateProgress(latestProgress);
					}

					@Override
					protected void done() {
						appendMessage("処理完了");
					}
				};

				worker.execute();
			}
		});
	}

	private void importCsvToTable(Connection conn, String csvDirPath, String tableName, boolean hasHeader)
			throws SQLException {
		File folder = new File(csvDirPath);
		File[] listOfFiles = folder.listFiles((dir, name) -> name.toUpperCase().endsWith(".CSV"));

		if (listOfFiles == null) {
			JOptionPane.showMessageDialog(null, "指定されたディレクトリにCSVファイルが見つかりません: " + csvDirPath);
			return;
		}

		conn.setAutoCommit(false);

		try (Statement stmt = conn.createStatement()) {
			stmt.execute("DROP TABLE IF EXISTS " + tableName);
		}

		for (File file : listOfFiles) {
			try (CSVReader csvReader = new CSVReaderBuilder(new InputStreamReader(new FileInputStream(file), "MS932"))
					.withSkipLines(0)
					.build()) {
				String[] headers = hasHeader ? csvReader.readNext() : generateDefaultHeaders(csvReader.peek());
				if (headers == null) {
					continue;
				}

				while (headers[0].startsWith("#")) {
					headers = csvReader.readNext();
					if (headers == null) {
						break;
					}
				}

				createTable(conn, tableName, headers);

				String insertSQL = generateInsertSQL(tableName, headers);
				try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) {
					String[] row;
					int count = 0;
					while ((row = csvReader.readNext()) != null) {
						if (row[0].startsWith("#")) {
							continue;
						}
						for (int i = 0; i < row.length; i++) {
							pstmt.setString(i + 1, row[i]);
						}
						pstmt.addBatch();
						if (++count % BATCH_SIZE == 0) {
							pstmt.executeBatch();
							conn.commit();
						}
					}
					pstmt.executeBatch(); // 残りのバッチを実行
					conn.commit();
				}
				appendMessage("CSVファイルをインポートしました: " + file.getName());
			} catch (IOException | CsvValidationException | SQLException e) {
				conn.rollback();
				e.printStackTrace();
				appendMessage("ファイルの処理中にエラーが発生しました " + file.getName() + ": " + e.getMessage());
			}
		}

		conn.setAutoCommit(true);
	}

	private String[] generateDefaultHeaders(String[] firstRow) {
		String[] headers = new String[firstRow.length];
		for (int i = 0; i < firstRow.length; i++) {
			headers[i] = "column" + (i + 1);
		}
		return headers;
	}

	private void createTable(Connection conn, String tableName, String[] headers) throws SQLException {
		List<String> columns = new ArrayList<>();
		for (String header : headers) {
			columns.add("\"" + header.replaceAll("\"", "\"\"") + "\" TEXT");
		}
		String sql = String.format("CREATE TABLE IF NOT EXISTS %s (%s)", tableName, String.join(",", columns));
		try (Statement stmt = conn.createStatement()) {
			stmt.execute(sql);
		}
	}

	private String generateInsertSQL(String tableName, String[] headers) {
		String[] columns = Arrays.stream(headers)
				.map(header -> "\"" + header.replaceAll("\"", "\"\"") + "\"")
				.toArray(String[]::new);
		String placeholders = String.join(",", Arrays.stream(headers).map(header -> "?").toArray(String[]::new));
		return String.format("INSERT INTO %s (%s) VALUES (%s)", tableName, String.join(",", columns), placeholders);
	}

	private void createIndexes(Connection conn, String tableName) throws SQLException {
		try (Statement stmt = conn.createStatement()) {
			String createIndexSQL = String.format("CREATE INDEX IF NOT EXISTS idx_%s_all ON %s (%s)",
					tableName, tableName, String.join(",", getColumnNames(conn, tableName)));
			stmt.execute(createIndexSQL);
		}
	}

	private String[] getColumnNames(Connection conn, String tableName) throws SQLException {
		List<String> columnNames = new ArrayList<>();
		try (Statement stmt = conn.createStatement();
				ResultSet rs = stmt.executeQuery("PRAGMA table_info(" + tableName + ")")) {
			while (rs.next()) {
				columnNames.add("\"" + rs.getString("name").replaceAll("\"", "\"\"") + "\"");
			}
		}
		return columnNames.toArray(new String[0]);
	}

	private void exportDiffToCsv(Connection conn, String tableA, String tableB, String outputDirPath,
			String outputFileName)
			throws SQLException, IOException {
		String sql = String.format("SELECT * FROM %s EXCEPT SELECT * FROM %s", tableA, tableB);
		CsvListWriter writer = null;
		try (Statement stmt = conn.createStatement();
				ResultSet rs = stmt.executeQuery(sql)) {

			ResultSetMetaData metaData = rs.getMetaData();
			int columnCount = metaData.getColumnCount();

			int fileCount = 1;
			int rowCount = 0;

			File outputDir = new File(outputDirPath);
			if (!outputDir.exists()) {
				outputDir.mkdirs();
			}
			String outputFilePath = String.format("%s/%s_比較先変更行_%03d.csv", outputDirPath, outputFileName, fileCount);
			writer = new CsvListWriter(
					new OutputStreamWriter(new FileOutputStream(outputFilePath), "MS932"), preference);
			//出力ファイルにヘッダーを出力するにチェックがある場合は実行する
			if (outPutHeaderCheckBox.isSelected()) {
				String[] headerRow = new String[columnCount];
				for (int i = 1; i <= columnCount; i++) {
					headerRow[i - 1] = metaData.getColumnName(i);
				}
				writer.write(headerRow);
			}
			while (rs.next()) {
				String[] row = new String[columnCount];
				for (int i = 1; i <= columnCount; i++) {
					row[i - 1] = rs.getString(i);
				}
				writer.write(row);

				rowCount++;
				if (rowCount % CHUNK_SIZE == 0) {
					writer.close();
					fileCount++;
					outputFilePath = String.format("%s/%s_比較先変更行_%03d.csv", outputDirPath, outputFileName, fileCount);
					writer = new CsvListWriter(
							new OutputStreamWriter(new FileOutputStream(outputFilePath), "MS932"), preference);
				}
			}
			appendMessage("差分をCSVにエクスポートしました: " + outputDirPath);
		} finally {
			if (Objects.nonNull(writer)) {
				writer.close();
			}
		}
	}

	private void exportDiffDelToCsv(Connection conn, String tableA, String tableB, String outputDirPath,
			String outputFileName)
			throws SQLException, IOException {
		String sql = String.format("SELECT * FROM %s EXCEPT SELECT * FROM %s", tableA, tableB);
		CsvListWriter writer = null;
		try (Statement stmt = conn.createStatement();
				ResultSet rs = stmt.executeQuery(sql)) {

			ResultSetMetaData metaData = rs.getMetaData();
			int columnCount = metaData.getColumnCount();

			int fileCount = 1;
			int rowCount = 0;

			File outputDir = new File(outputDirPath);
			if (!outputDir.exists()) {
				outputDir.mkdirs();
			}

			String outputFilePath = String.format("%s/%s_比較元変更行_%03d.csv", outputDirPath, outputFileName, fileCount);
			writer = new CsvListWriter(
					new OutputStreamWriter(new FileOutputStream(outputFilePath), "MS932"), preference);
			//出力ファイルにヘッダーを出力するにチェックがある場合は実行する
			if (outPutHeaderCheckBox.isSelected()) {
				String[] headerRow = new String[columnCount];
				for (int i = 1; i <= columnCount; i++) {
					headerRow[i - 1] = metaData.getColumnName(i);
				}
				writer.write(headerRow);
			}
			while (rs.next()) {
				String[] row = new String[columnCount];
				for (int i = 1; i <= columnCount; i++) {
					row[i - 1] = rs.getString(i);
				}
				writer.write(row);

				rowCount++;
				if (rowCount % CHUNK_SIZE == 0) {
					writer.close();
					fileCount++;
					outputFilePath = String.format("%s/%s_比較元変更行_%03d.csv", outputDirPath, outputFileName, fileCount);
					writer = new CsvListWriter(
							new OutputStreamWriter(new FileOutputStream(outputFilePath), "MS932"), preference);
				}
			}
			appendMessage("差分をCSVにエクスポートしました: " + outputDirPath);
		} finally {
			if (Objects.nonNull(writer)) {
				writer.close();
			}
		}
	}

	private void updateColumnSelectionPanel(Connection conn) throws SQLException {
		columnSelectionPanel.removeAll();
		columnCheckBoxes.clear();

		String query = "PRAGMA table_info(before)";
		try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query)) {
			GridBagConstraints gbc = new GridBagConstraints();
			gbc.fill = GridBagConstraints.HORIZONTAL;
			gbc.insets = new Insets(5, 5, 5, 5);
			gbc.gridx = 0;
			int row = 0;

			while (rs.next()) {
				String columnName = rs.getString("name");
				JCheckBox checkBox = new JCheckBox(columnName);
				columnCheckBoxes.add(checkBox);
				gbc.gridy = row++;
				columnSelectionPanel.add(checkBox, gbc);
			}
		}

		columnSelectionPanel.revalidate();
		columnSelectionPanel.repaint();
	}

	private synchronized void updateProgress(int percent) {
		progressBar.setValue(percent);
	}

	private synchronized void appendMessage(String message) {
		messageArea.append(message + "\n");
	}

	public static void main(String[] args) {
		SwingUtilities.invokeLater(() -> new CsvToSQLiteApp().setVisible(true));
	}
}




```


1. **カラムリストの追加**：
   - `JList<String> columnList`を追加し、複数選択可能なリストとして設定しました。
   - CSVファイルのインポート時に、カラム名を取得してリストに追加しています。

2. **出力時のカラム順の変更**：
   - `exportDiffToCsv`および`exportDiffDelToCsv`メソッドで、選択されたカラム順にデータをエクスポートするようにしました。
   - 選択されたカラムリストを取得し、SQLクエリを動的に生成するようにしています。

これにより、ユーザーはGUIでカラムを選択し、出力順を変更できるようになります。


### ソフトウェアの機能概要

1. **GUIインタフェース**
   - ユーザーが比較元と比較先のディレクトリパスを選択し、出力先のCSVファイルパスを指定できます。
   - ヘッダー行の有無を選択するチェックボックスが提供されています。

2. **データベース操作**
   - SQLiteデータベースを使用して、比較元と比較先のCSVファイルをそれぞれ「before」と「after」という名前のテーブルにインポートします。
   - CSVファイル内のコメント行（行の先頭に`#`がついている行）は無視されます。

3. **差分の抽出とCSV出力**
   - SQLiteのクエリを使用して、「before」と「after」テーブルの差分を検出します。
   - 差分が見つかった場合、差分をSJISエンコードされたCSVファイルとして指定されたパスに出力します。

4. **進捗とメッセージの表示**
   - 処理中の進捗状況をパーセンテージで表示し、GUIのプログレスバーで視覚化します。
   - メッセージエリアには、処理中に発生した重要な情報やエラーメッセージが表示されます。

5. **その他**
   - ユーザーが選択したディレクトリからCSVファイルを読み込み、OpenCSVライブラリを使用してCSVデータを処理します。
   - テーブル作成時やインポート時にエラーが発生した場合、エラーメッセージがダイアログとメッセージエリアに表示されます。

### 技術的詳細

- **言語**: Java 17
- **GUIフレームワーク**: Swing
- **データベース**: SQLite
- **CSV操作**: OpenCSVライブラリを使用してCSVの読み込みと書き出しを行います。
- **文字エンコーディング**: 比較元と比較先のCSVファイルはSJISで定義されており、JavaのInputStreamReaderを使用してSJISエンコーディングで読み込みます。

### ソースコードのポイント

- `SwingWorker`を使用して、バックグラウンドでデータベース操作とCSV処理を行い、進捗状況を更新します。
- CSVファイルからヘッダーを読み込むか、デフォルトの列名を生成するメソッドを提供し、選択肢をユーザーに委ねます。
- SQLiteでのテーブル作成、インデックスの作成、差分の抽出とCSV出力を効率的に行うためのバッチ処理を実装しています。

この改良されたソースコードを参考にして、さらに必要に応じてカスタマイズや機能の追加を行うことができます。

必要な依存関係を含むMavenプロジェクトの`pom.xml`ファイルの例です。
このプロジェクトは、SQLite JDBCドライバとOpenCSVライブラリを使用します。

### `pom.xml`

```xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>csv-to-sqlite-app</artifactId>
	<version>1.0-SNAPSHOT</version>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>20</maven.compiler.source>
		<maven.compiler.target>20</maven.compiler.target>
	</properties>

	<dependencies>
		<!-- SQLite JDBC Driver -->
		<dependency>
			<groupId>org.xerial</groupId>
			<artifactId>sqlite-jdbc</artifactId>
			<version>3.34.0</version>
		</dependency>
		<!-- OpenCSV Library -->
		<dependency>
			<groupId>com.opencsv</groupId>
			<artifactId>opencsv</artifactId>
			<version>5.5.2</version>
		</dependency>
		<!-- JUnit for testing (optional) -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.13.2</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>net.sf.supercsv</groupId>
			<artifactId>super-csv</artifactId>
			<version>2.4.0</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<source>20</source>
					<target>20</target>
				</configuration>
			</plugin>

			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-assembly-plugin</artifactId>
				<version>3.3.0</version>
				<configuration>
					<archive>
						<manifest>
							<mainClass>
								jp.co.hirogin.batch_ikou_dff.CsvToSQLiteApp</mainClass>
						</manifest>
					</archive>
					<descriptorRefs>
						<descriptorRef>jar-with-dependencies</descriptorRef>
					</descriptorRefs>
				</configuration>
				<executions>
					<execution>
						<id>make-assembly</id>
						<phase>package</phase>
						<goals>
							<goal>single</goal>
						</goals>
					</execution>
				</executions>
			</plugin>

			<plugin>
				<artifactId>maven-resources-plugin</artifactId>
				<version>3.2.0</version>
				<executions>
					<execution>
						<id>copy-jre</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>copy-resources</goal>
						</goals>
						<configuration>
							<outputDirectory>${project.build.directory}/jre</outputDirectory>
							<resources>
								<resource>
									<directory>${project.basedir}/jre</directory>
								</resource>
							</resources>
						</configuration>
					</execution>
				</executions>
			</plugin>

			<plugin>
				<groupId>com.akathist.maven.plugins.launch4j</groupId>
				<artifactId>launch4j-maven-plugin</artifactId>
				<version>2.1.2</version>
				<executions>
					<execution>
						<id>l4j-clui</id>
						<phase>package</phase>
						<goals>
							<goal>launch4j</goal>
						</goals>
						<configuration>
							<headerType>console</headerType>
							<jar>
								${project.build.directory}/${project.artifactId}-${project.version}-jar-with-dependencies.jar</jar>
							<outfile>
								${project.build.directory}/差分比較君.exe</outfile>
							<classPath>
								<mainClass>
									jp.co.hirogin.batch_ikou_dff.CsvToSQLiteApp</mainClass>
								<preCp>anything</preCp>
							</classPath>
							<jre>
								<path>jre</path>
								<bundledJre64Bit>true</bundledJre64Bit>
								<bundledJreAsFallback>false</bundledJreAsFallback>
								<minVersion>20</minVersion>
								<jdkPreference>jreOnly</jdkPreference>
								<runtimeBits>64</runtimeBits>
							</jre>
							<dontWrapJar>false</dontWrapJar>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>
</project>
```

### 説明

- **`<dependencies>` セクション**:
  - SQLite JDBCドライバ（`org.xerial:sqlite-jdbc:3.34.0`）
  - OpenCSVライブラリ（`com.opencsv:opencsv:5.5.2`）
  - （オプション）JUnit 4.13.2（テストのため）

- **`<build>` セクション**:
  - `maven-compiler-plugin`を使用してJavaコンパイラのバージョンを指定しています。この例ではJava 17を使用しています。

この`pom.xml`ファイルを使用することで、プロジェクトに必要な依存関係が自動的にダウンロードおよび設定されます。これにより、CSVファイルをSQLiteデータベースに取り込み、差分を抽出してCSVファイルに出力するJava Swingアプリケーションを構築できます。

### JREを同梱
JREを同梱したアプリケーションとしてリリースする場合、以下の手順に従って準備することができます。

1. プロジェクトのビルド:
   まず、プロジェクトを JAR ファイルにビルドします。

2. カスタム JRE の作成:
   Java 9 以降では、jlink ツールを使用してカスタム JRE を作成できます。

3. アプリケーションのパッケージング:
   アプリケーションと JRE を一緒にパッケージングします。

4. 起動スクリプトの作成:
   Windows と Unix 系 OS 用の起動スクリプトを作成します。

以下に、これらの手順の詳細を示します：

1. プロジェクトのビルド:
   Maven や Gradle を使用して JAR ファイルを作成します。pom.xml や build.gradle ファイルに必要な依存関係（OpenCSV、SQLite JDBC）を追加します。

2. カスタム JRE の作成:
   jlink を使用してカスタム JRE を作成します。必要なモジュールのみを含めることで、サイズを最小限に抑えられます。

   ```
   jlink --module-path $JAVA_HOME/jmods --add-modules java.base,java.desktop,java.sql --output custom-jre
   ```

3. アプリケーションのパッケージング:
   以下のようなディレクトリ構造を作成します：

   ```
   CsvToSQLiteApp/
   ├── app/
   │   ├── CsvToSQLiteApp.jar
   │   ├── lib/
   │   │   ├── opencsv-5.5.jar
   │   │   └── sqlite-jdbc-3.34.0.jar
   ├── jre/
   │   └── (custom JRE files)
   ├── run.bat
   └── run.sh
   ```

4. 起動スクリプトの作成:
   Windows 用 (run.bat):
   ```batch
   @echo off
   start .\jre\bin\javaw.exe -jar .\app\CsvToSQLiteApp.jar
   ```

   Unix 系 OS 用 (run.sh):
   ```bash
   #!/bin/bash
   ./jre/bin/java -jar ./app/CsvToSQLiteApp.jar
   ```

5. 配布用アーカイブの作成:
   上記のディレクトリ構造全体を ZIP ファイルにまとめます。

追加の考慮事項:

1. ライセンス: JRE を配布する際の Oracle のライセンス条項を確認してください。OpenJDK を使用することで、ライセンスの問題を回避できる場合があります。

2. セキュリティ: アプリケーションの更新方法を考慮し、セキュリティパッチを適用できるようにしてください。

3. インストーラー: より洗練された配布方法として、インストーラーの作成を検討してください。Launch4j や Install4j などのツールが役立ちます。

4. マルチプラットフォーム: 異なる OS 向けに別々のパッケージを用意することを検討してください。

5. 署名: Windows 上でセキュリティ警告を減らすために、アプリケーションに電子署名することを検討してください。

これらの手順に従うことで、JRE を同梱した独立したアプリケーションとしてリリースできます。ユーザーは Java をシステムにインストールすることなく、アプリケーションを実行できるようになります。





## ディレクトリ構成とexe作成手順

### ディレクトリ構成

```
プロジェクト名/
├── target/
│   └── classes/
│       └── com/
│           └── example/
│               └── CsvToSQLiteApp.class
├── src/
│   └── main/
│       └── java/
│           └── com/
│               └── example/
│                   └── CsvToSQLiteApp.java
├── pom.xml
├── jre/
│   └── (JREのファイル群)
├── launch4j.xml
└── MyApp.exe
```

* **target/classes:** コンパイルされたクラスファイルが格納されます。
* **src/main/java:** ソースコードが格納されます。
* **pom.xml:** Mavenのプロジェクト設定ファイルです。
* **jre:** JREのファイルが格納されます。
* **launch4j.xml:** launch4jの設定ファイルです。
* **MyApp.exe:** 作成される実行ファイルです。

### exe作成手順

1. **Mavenでプロジェクトをビルド:**
   ```bash
   mvn clean package
   ```
   これにより、target/classesディレクトリにクラスファイルが生成されます。

2. **JREの準備:**
   * Oracleの公式サイトから、ターゲットとするJavaのバージョンに対応したJREをダウンロードします。
   * ダウンロードしたJREを、プロジェクトディレクトリのjreフォルダに解凍します。

3. **launch4jの設定:**
   launch4j.xmlファイルを以下の内容で作成します。

   ```xml
   <?xml version="1.0" encoding="UTF-8"?>
   <launch4j configVersion="9.2.1">
     <header>
       <identifier>MyApp</identifier>
       <version>1.0.0</version>
       <file>MyApp.exe</file>
       <icon>icon.ico</icon> <url/>
       <classname>com.example.CsvToSQLiteApp</classname>
       <mainClass>com.example.CsvToSQLiteApp</mainClass>
     </header>
     <jre>
       <path>jre</path>
       <minVersion>11.0.0</minVersion> <maxVersion/>
       <initialHeapSize>128</initialHeapSize>
       <maxHeapSize>1024</maxHeapSize>
     </jre>
   </launch4j>
   ```

4. **exeファイルの作成:**
   コマンドプロンプトで以下のコマンドを実行します。

   ```bash
   launch4j -o MyApp.exe launch4j.xml
   ```

### 注意点

* **JREのパス:** launch4j.xmlの<jre>タグの<path>属性で指定するパスは、exeファイルからの相対パスです。
* **メインクラス:** <classname>と<mainClass>には、実行したいメインクラスを指定します。
* **アイコン:** <icon>属性にアイコンファイルのパスを指定することで、exeファイルにアイコンを設定できます。
* **最小Javaバージョン:** <minVersion>属性に、実行に必要な最小Javaバージョンを指定します。

### まとめ

上記の手順に従うことで、JREを同梱した実行ファイルを作成できます。このexeファイルを配布することで、ユーザーは別途Java環境を用意することなく、アプリケーションを実行することができます。

**補足:**

* launch4jの設定項目は他にも多数あります。詳細については、launch4jの公式ドキュメントを参照してください。
* インストーラーを作成したい場合は、Inno Setupなどのインストーラー作成ツールと組み合わせることができます。


これまでの内容を要約します。

### Mavenで`exe`ファイルを作成する手順

1.