システム要件とアーキテクチャ設計
本記事では、Javaを用いてコマンドラインベースの学生情報管理アプリケーションを構築します。開発は2つの段階に分け、まずコアとなるデータ操作機能を実装し、その後セキュアなアクセス制御層を追加します。
- 基本階層: 学籍レコードの作成・検索・更新・削除(CRUD)を実装
- 拡張階層: ユーザーアカウント管理、入力値の正規表現検証、セッション遷移制御
エンティティクラスの定義
データ構造を明確に分離するため、学生情報用クラスとユーザー認証用クラスを作成します。フィールドにはアクセサーとミューテーターを提供し、初期値設定メソッドを一元管理します。
// Student.java
public class StudentRecord {
private String fullName;
private int studentId;
private Integer age;
private String residence;
public StudentRecord() {}
public StudentRecord(String fullName, int studentId, Integer age, String residence) {
this.fullName = fullName;
this.studentId = studentId;
this.age = age;
this.residence = residence;
}
// アクセサー
public String getFullName() { return fullName; }
public void setFullName(String name) { this.fullName = name; }
public int getStudentId() { return studentId; }
public void setStudentId(int id) { this.studentId = id; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public String getResidence() { return residence; }
public void setResidence(String loc) { this.residence = loc; }
public void displayDetails() {
System.out.printf("%-8d %-15s %-5d %-10s%n", studentId, fullName, age, residence);
}
}
// UserAccount.java
public class UserAccount {
private String username;
private String password;
private String identificationNumber;
private String contactPhone;
public UserAccount() {}
public UserAccount(String username, String password, String idNumber, String phone) {
this.username = username;
this.password = password;
this.identificationNumber = idNumber;
this.contactPhone = phone;
}
public String getUsername() { return username; }
public void setUsername(String user) { this.username = user; }
public String getPassword() { return password; }
public void setPassword(String pwd) { this.password = pwd; }
public String getIdNumber() { return identificationNumber; }
public void setIdNumber(String num) { this.identificationNumber = num; }
public String getContactPhone() { return contactPhone; }
public void setContactPhone(String phone) { this.contactPhone = phone; }
}
データ操作ロジックの実装
コレクションフレームワークを使用してメモリ内のデータストアを管理します。重複判定や検索処理はストリームAPIを活用し、可読性と保守性を向上させます。
import java.util.*;
public class StudentDataManager {
private List<StudentRecord> records = new ArrayList<>();
public StudentDataManager() {
records.add(new StudentRecord("Tanaka", 1001, 20, "Tokyo"));
}
private Scanner console = new Scanner(System.in);
private static final Map<String, Runnable> menuActions = Map.of(
"1", this::registerNewStudent,
"2", this::searchAllStudents,
"3", this::updateStudentInfo,
"4", this::removeStudentById,
"5", null // Exit handled externally
);
public boolean executeMainLoop() {
while (true) {
printMenu();
String selection = console.nextLine().trim();
Runnable action = menuActions.get(selection);
if (action == null && !selection.equals("5")) {
System.out.println("無効な選択です。再度入力してください。");
continue;
}
if (action != null) {
action.run();
} else {
System.out.println("プログラムを終了しました。");
return true;
}
}
}
private void printMenu() {
System.out.println("=== 学生管理メニュー ===");
System.out.println("1: 新規登録 | 2: 全件検索 | 3: 情報更新 | 4: 削除 | 5: 終了");
System.out.print("オプションを選択: ");
}
public void registerNewStudent() {
System.out.print("氏名: "); String name = console.nextLine();
System.out.print("年齢: "); int age = console.nextInt(); console.nextLine();
System.out.print("居住地: "); String loc = console.nextLine();
System.out.print("学籍番号: "); int id = console.nextInt(); console.nextLine();
long duplicateCheck = records.stream().filter(r -> r.getStudentId() == id).count();
if (duplicateCheck > 0) {
System.out.println("エラー: 指定された学籍番号は既に使用されています。");
} else {
records.add(new StudentRecord(name, id, age, loc));
System.out.println("登録が完了しました。");
}
}
public void searchAllStudents() {
if (records.isEmpty()) {
System.out.println("対象データが存在しません。");
return;
}
System.out.printf("%n%-8s %-15s %-5s %-10s%n", "番号", "氏名", "年齢", "居住地");
records.forEach(StudentRecord::displayDetails);
}
public void updateStudentInfo() {
System.out.print("更新対象の学籍番号を入力: ");
int targetId = console.nextInt(); console.nextLine();
Optional<StudentRecord> target = records.stream()
.filter(r -> r.getStudentId() == targetId)
.findFirst();
if (target.isPresent()) {
System.out.println("現在の居住地: " + target.get().getResidence());
System.out.print("新しい居住地: ");
target.get().setResidence(console.nextLine());
System.out.println("更新処理が正常に完了しました。");
} else {
System.out.println("該当する学籍番号が見つかりません。");
}
}
public void removeStudentById() {
System.out.print("削除対象の学籍番号: ");
int removeId = console.nextInt(); console.nextLine();
boolean removed = records.removeIf(r -> r.getStudentId() == removeId);
System.out.println(removed ? "削除が成功しました。" : "指定されたIDのレコードは存在しません。");
}
}
認証フローとバリデーションの実装
セキュリティ要件を満たすため、ユーザー情報の一意性チェック、フォーマット検証、および試行回数制限を導入します。正規表現パターンを使用し、入力サニタイジングを適用します。
import java.util.*;
import java.util.regex.*;
public class AuthenticationController {
private List<UserAccount> accounts = new ArrayList<>();
private Scanner scanner = new Scanner(System.in);
private static final Pattern USER_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9]{2,14}$");
private static final Pattern ID_CARD_PATTERN = Pattern.compile("^[1-9]\\d{16}[0-9Xx]$");
private static final Pattern PHONE_PATTERN = Pattern.compile("^\\d{10,11}$");
public AuthenticationController() {
accounts.add(new UserAccount("admin_user", "pass123", "123456789012345678", "09012345678"));
}
public boolean runAuthSequence() {
System.out.println("--- ユーザー認証画面 ---");
System.out.println("1: ログイン | 2: 新規登録 | 3: パスワード再設定 | 4: 退出");
String choice = scanner.nextLine().trim();
switch (choice) {
case "1": initiateLogin(); break;
case "2": initiateRegistration(); break;
case "3": initiatePasswordReset(); break;
case "4": return true;
default: System.out.println("無効なオプションです。");
}
return false;
}
private boolean validateFormat(String input, Pattern pattern) {
return pattern.matcher(input).matches();
}
private void initiateRegistration() {
System.out.print("ユーザー名: "); String user = scanner.nextLine();
if (!USER_PATTERN.matcher(user).matches()) {
System.out.println("ユーザー名の形式が不正です。(半角英数3〜15桁、数字のみ不可)");
return;
}
System.out.print("パスワード: "); String pass1 = scanner.nextLine();
System.out.print("確認用パスワード: "); String pass2 = scanner.nextLine();
if (!pass1.equals(pass2)) {
System.out.println("パスワードが一致しません。");
return;
}
System.out.print("本人確認番号(18桁): "); String idNum = scanner.nextLine();
if (!validateFormat(idNum, ID_CARD_PATTERN)) {
System.out.println("本人確認番号の形式が不正です。");
return;
}
System.out.print("携帯電話番号(10-11桁): "); String phone = scanner.nextLine();
if (!validateFormat(phone, PHONE_PATTERN)) {
System.out.println("携帯電話番号の形式が不正です。");
return;
}
if (accounts.stream().anyMatch(a -> a.getUsername().equals(user))) {
System.out.println("このユーザー名は既に登録されています。");
} else {
accounts.add(new UserAccount(user, pass1, idNum, phone));
System.out.println("アカウントの作成が完了しました。");
}
}
private void initiateLogin() {
System.out.print("ユーザー名: "); String uname = scanner.nextLine();
Optional<UserAccount> accountOpt = accounts.stream()
.filter(a -> a.getUsername().equals(uname))
.findFirst();
if (accountOpt.isEmpty()) {
System.out.println("エラー: 未登録のユーザーです。事前に登録が必要です。");
return;
}
String captcha = generateCaptcha();
System.out.printf("確認コード: [%s]%n", captcha);
System.out.print("コードを入力: "); String userInput = scanner.nextLine();
if (!userInput.equalsIgnoreCase(captcha)) {
System.out.println("確認コードが正しくありません。");
return;
}
int attempts = 0;
while (attempts < 3) {
System.out.print("パスワード: "); String inputPass = scanner.nextLine();
if (accountOpt.get().getPassword().equals(inputPass)) {
System.out.println("認証に成功しました。学生管理システムへ移行します。");
new StudentDataManager().executeMainLoop();
return;
}
attempts++;
System.out.printf("失敗しました。残り %d 回の試行可能%n", 3 - attempts);
}
System.out.println("最大試行回数 exceeded。ログアウトします。");
}
private void initiatePasswordReset() {
System.out.print("ユーザー名: "); String uname = scanner.nextLine();
Optional<UserAccount> targetAccount = accounts.stream()
.filter(a -> a.getUsername().equals(uname))
.findFirst();
if (targetAccount.isEmpty()) {
System.out.println("エラー: 該当アカウントが見つかりません。");
return;
}
System.out.print("本人確認番号: "); String checkId = scanner.nextLine();
System.out.print("携帯電話番号: "); String checkPhone = scanner.nextLine();
if (targetAccount.get().getIdNumber().equals(checkId) &&
targetAccount.get().getContactPhone().equals(checkPhone)) {
System.out.print("新しいパスワード: ");
targetAccount.get().setPassword(scanner.nextLine());
System.out.println("パスワードの変更が正常に反映されました。");
} else {
System.out.println("エラー: 登録済みの個人情報と一致しません。変更できません。");
}
}
private String generateCaptcha() {
Random rand = new Random();
StringBuilder builder = new StringBuilder();
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
for (int i = 0; i < 4; i++) builder.append(chars.charAt(rand.nextInt(chars.length())));
builder.insert(rand.nextInt(4), rand.nextInt(10));
return builder.toString();
}
}
実行フローの統合
上記のモジュールを組み合わせることで、入出力処理、ビジネスロジック、データ永続化(メモリ上)が分離された構成を実現します。コンソール経由で起動すれば、登録→認証→CRUD操作までの一連のワークフローをテスト可能です。