|
|
|
@ -1,9 +1,13 @@ |
|
|
|
package com.example.cli; |
|
|
|
|
|
|
|
import java.util.ArrayList; |
|
|
|
import java.util.Arrays; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.List; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.Scanner; |
|
|
|
import java.util.regex.Matcher; |
|
|
|
import java.util.regex.Pattern; |
|
|
|
|
|
|
|
import com.example.controller.NoteController; |
|
|
|
import com.example.controller.TagController; |
|
|
|
@ -12,22 +16,80 @@ import com.example.service.TagService; |
|
|
|
import com.example.service.storage.JsonStorageService; |
|
|
|
import com.example.service.storage.StorageService; |
|
|
|
|
|
|
|
/** |
|
|
|
* 教学注释:CommandParser (命令解析器) |
|
|
|
* |
|
|
|
* 设计目的: |
|
|
|
* 1. **关注点分离 (Separation of Concerns)**:此类是整个CLI应用的用户界面层 (User Interface Layer)。 |
|
|
|
* 它的唯一职责是:接收用户的输入(无论是通过命令行参数还是交互式会话),解析这些输入,然后调用相应的业务逻辑(控制器)。 |
|
|
|
* 它不关心笔记如何创建、存储或搜索,这些都委托给其他类。这是软件设计中最重要的原则之一。 |
|
|
|
* |
|
|
|
* 2. **用户交互的入口**:作为CLI应用的“大脑”,所有用户交互都从这里开始。它决定了应用是进入交互模式,还是执行单个命令后退出。 |
|
|
|
* |
|
|
|
* 3. **可扩展性**:通过 `switch` 结构和独立的 `handle...` 方法,添加新命令变得非常简单。 |
|
|
|
* 只需在 `switch` 中增加一个 `case`,并实现一个新的 `handle...` 方法即可,不会影响到现有命令的逻辑。 |
|
|
|
* |
|
|
|
* 在经典分层架构中,这个类扮演着 "表示层" (Presentation Layer) 或 "视图/控制器" (View/Controller) 的角色。 |
|
|
|
* - 视图 (View):System.out.println() 和 System.in (控制台的输入输出)。 |
|
|
|
* - 控制器 (Controller):解析命令并调用后端服务。 |
|
|
|
*/ |
|
|
|
public class CommandParser { |
|
|
|
// --- 成员变量:定义了类的状态和依赖 ---
|
|
|
|
|
|
|
|
/** |
|
|
|
* 教学注释:Scanner 用于读取用户在交互模式下的输入。 |
|
|
|
* 它是Java标准库中处理控制台输入的标准工具。 |
|
|
|
*/ |
|
|
|
private Scanner scanner; |
|
|
|
|
|
|
|
/** |
|
|
|
* 教学注释:isRunning 是一个状态标志,用于控制交互式模式的循环。 |
|
|
|
* 当用户输入 "exit" 或 "quit" 时,此标志变为 false,主循环终止,程序退出。 |
|
|
|
* 这种 "状态驱动" 的循环是事件驱动编程中的常见模式。 |
|
|
|
*/ |
|
|
|
private boolean isRunning; |
|
|
|
|
|
|
|
// 服务和控制器实例
|
|
|
|
/** |
|
|
|
* 教学注释:依赖注入 (Dependency Injection) 的简化实现。 |
|
|
|
* |
|
|
|
* 设计目的: |
|
|
|
* CommandParser 依赖于各种服务 (Service) 和控制器 (Controller) 来完成实际工作。 |
|
|
|
* 不在每个方法中创建这些对象,而是在构造函数中一次性创建并持有它们的引用。 |
|
|
|
* |
|
|
|
* 优点: |
|
|
|
* 1. **解耦**:CommandParser 不关心 NoteService 的具体实现是 JsonStorageService 还是未来的数据库实现。 |
|
|
|
* 如果未来要更换存储方式(比如从JSON换成数据库),只需修改构造函数中的 `new JsonStorageService()` 即可, |
|
|
|
* 所有 `handle...` 方法的代码都无需改动。 |
|
|
|
* 2. **性能**:避免了在每次命令执行时重复创建和销毁这些重量级对象。 |
|
|
|
* 3. **可测试性**:在单元测试中,我们可以传入这些依赖的 "模拟" (Mock) 版本,从而可以独立测试 CommandParser 的逻辑, |
|
|
|
* 而无需一个真实的JSON文件。 |
|
|
|
* |
|
|
|
* 这里的 `final` 关键字确保这些依赖在对象创建后不能被更改,增加了程序的健壮性。 |
|
|
|
*/ |
|
|
|
private final StorageService storageService; |
|
|
|
private final NoteService noteService; |
|
|
|
private final TagService tagService; |
|
|
|
private final NoteController noteController; |
|
|
|
private final TagController tagController; |
|
|
|
|
|
|
|
/** |
|
|
|
* 教学注释:构造函数 (Constructor) |
|
|
|
* |
|
|
|
* 设计目的: |
|
|
|
* 对象的初始化入口。当一个 `CommandParser` 对象被创建时,这个构造函数会执行。 |
|
|
|
* 它的核心任务是 "装配" (Wire) 应用的所有组件,确保对象在被使用之前处于一个完整、可用的状态。 |
|
|
|
* |
|
|
|
* 这里完成了: |
|
|
|
* 1. 初始化用于交互模式的 `Scanner` 和 `isRunning` 标志。 |
|
|
|
* 2. 创建所有依赖的服务和控制器实例,建立它们之间的依赖关系。 |
|
|
|
* 例如,`NoteService` 依赖 `storageService`,`NoteController` 依赖 `noteService`。 |
|
|
|
* 这种依赖链的构建是应用启动时的关键步骤。 |
|
|
|
*/ |
|
|
|
public CommandParser() { |
|
|
|
this.scanner = new Scanner(System.in); |
|
|
|
this.isRunning = true; |
|
|
|
|
|
|
|
// 初始化服务和控制器
|
|
|
|
// 初始化服务和控制器,建立依赖关系
|
|
|
|
this.storageService = new JsonStorageService(); |
|
|
|
this.noteService = new NoteService(storageService); |
|
|
|
this.tagService = new TagService(storageService); |
|
|
|
@ -36,20 +98,43 @@ public class CommandParser { |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 解析命令行参数 |
|
|
|
* 教学注释:解析命令行参数 (入口方法) |
|
|
|
* |
|
|
|
* @param args 从 `main` 方法传递过来的原始命令行参数数组。 |
|
|
|
* |
|
|
|
* 设计目的: |
|
|
|
* 这是应用的第一个逻辑分支点。它决定了应用的工作模式。 |
|
|
|
* 1. **无参数** (`args.length == 0`):用户直接运行程序,没有附加任何命令。 |
|
|
|
* 此时,应用进入一个持续运行的 "交互式会话" (Interactive Session),等待用户逐条输入命令。 |
|
|
|
* 2. **有参数**:用户在启动程序时就指定了一个完整的命令(例如 `java -jar pkm.jar new "My Title" "My Content"`)。 |
|
|
|
* 此时,应用执行该命令,完成后立即退出。这对于脚本自动化非常有用。 |
|
|
|
* |
|
|
|
* `String.join(" ", args)` 的作用是将 `["new", "\"My Title\"", "\"My Content\""]` 这样的数组重新组合成 |
|
|
|
* `"new "My Title" "My Content""` 这样的单行字符串,以便后续的 `executeCommand` 方法可以统一处理。 |
|
|
|
*/ |
|
|
|
public void parseArgs(String[] args) { |
|
|
|
if (args.length == 0) { |
|
|
|
// 进入交互模式
|
|
|
|
// 如果没有提供命令行参数,则进入交互模式
|
|
|
|
startInteractiveMode(); |
|
|
|
} else { |
|
|
|
// 执行单个命令
|
|
|
|
// 如果有命令行参数,则将它们拼接成一个字符串并执行单个命令
|
|
|
|
executeCommand(String.join(" ", args)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 启动交互式命令行模式 |
|
|
|
* 教学注释:启动交互式命令行模式 |
|
|
|
* |
|
|
|
* 设计目的: |
|
|
|
* 实现一个经典的 "读取-求值-打印循环" (Read-Eval-Print Loop, REPL)。 |
|
|
|
* 这是许多开发工具(如 Python 解释器、j-shell、Node.js 控制台)的核心交互模式。 |
|
|
|
* |
|
|
|
* 循环流程: |
|
|
|
* 1. **打印提示符** (`pkm> `):告知用户系统已准备好接收命令。 |
|
|
|
* 2. **读取 (Read)**:使用 `scanner.nextLine()` 等待并获取用户输入的一整行。 |
|
|
|
* 3. **求值 (Eval)**:调用 `executeCommand(input)` 来处理这条命令。 |
|
|
|
* 4. **打印 (Print)**:`executeCommand` 的结果(成功信息或错误提示)被打印到控制台。 |
|
|
|
* 5. **循环 (Loop)**:只要 `isRunning` 标志为 `true`,就重复以上步骤。 |
|
|
|
*/ |
|
|
|
private void startInteractiveMode() { |
|
|
|
System.out.println("欢迎使用个人知识管理系统 (CLI版)"); |
|
|
|
@ -57,24 +142,40 @@ public class CommandParser { |
|
|
|
|
|
|
|
while (isRunning) { |
|
|
|
System.out.print("pkm> "); |
|
|
|
String input = scanner.nextLine().trim(); |
|
|
|
String input = scanner.nextLine().trim(); // .trim() 用于移除输入前后多余的空格
|
|
|
|
|
|
|
|
if (!input.isEmpty()) { |
|
|
|
if (!input.isEmpty()) { // 避免用户只敲回车时执行空命令
|
|
|
|
executeCommand(input); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 执行命令 |
|
|
|
* 教学注释:执行命令 (命令分发器) |
|
|
|
* |
|
|
|
* @param commandLine 完整的单行命令字符串。 |
|
|
|
* |
|
|
|
* 设计目的: |
|
|
|
* 这是命令处理的核心枢纽,也称为 "分发器" (Dispatcher)。 |
|
|
|
* |
|
|
|
* 它的职责是: |
|
|
|
* 1. **解析**:将命令字符串 (`"new "My Title" "My Content""`) 分解成命令和参数数组 (`["new", "\"My Title\"", "\"My Content\""]`)。 |
|
|
|
* 这一步通过 `parseCommandLine` 方法完成,该方法能够正确处理带引号的参数。 |
|
|
|
* 2. **标准化**:将命令本身(如 "NEW", "New")转换为小写(`"new"`),使得命令匹配不区分大小写,提升用户体验。 |
|
|
|
* 3. **分发**:使用 `switch` 语句,根据命令将执行流程导向到对应的 `handle...` 方法。 |
|
|
|
* `switch` 是处理固定命令集的最高效、最清晰的方式。 |
|
|
|
* 4. **委托**:将解析出的参数 (`args`) 传递给 `handle...` 方法,让它们去完成具体的工作。 |
|
|
|
*/ |
|
|
|
private void executeCommand(String commandLine) { |
|
|
|
// 1. 解析命令行,支持带引号的参数
|
|
|
|
String[] parts = parseCommandLine(commandLine); |
|
|
|
if (parts.length == 0) return; |
|
|
|
if (parts.length == 0) return; // 如果是空命令,直接返回
|
|
|
|
|
|
|
|
String command = parts[0].toLowerCase(); |
|
|
|
String[] args = Arrays.copyOfRange(parts, 1, parts.length); |
|
|
|
// 2. 分离命令和参数
|
|
|
|
String command = parts[0].toLowerCase(); // 命令不区分大小写
|
|
|
|
String[] args = Arrays.copyOfRange(parts, 1, parts.length); // 提取参数
|
|
|
|
|
|
|
|
// 3. 使用 switch 语句进行命令分发
|
|
|
|
switch (command) { |
|
|
|
case "new": |
|
|
|
handleNewCommand(args); |
|
|
|
@ -116,7 +217,7 @@ public class CommandParser { |
|
|
|
handleHelpCommand(); |
|
|
|
break; |
|
|
|
case "exit": |
|
|
|
case "quit": |
|
|
|
case "quit": // 支持多个退出命令
|
|
|
|
handleExitCommand(); |
|
|
|
break; |
|
|
|
default: |
|
|
|
@ -126,14 +227,63 @@ public class CommandParser { |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 解析命令行,支持引号包围的参数 |
|
|
|
* 教学注释:解析命令行,支持引号包围的参数 |
|
|
|
* |
|
|
|
* @param commandLine 原始命令行字符串 |
|
|
|
* @return 解析后的字符串数组 |
|
|
|
* |
|
|
|
* 设计目的: |
|
|
|
* 简单的 `commandLine.split(" ")` 无法处理包含空格的参数,例如 `new "My Title" "Content"`. |
|
|
|
* 它会错误地把 `"My` 和 `Title"` 分开。我们需要一个更智能的解析器。 |
|
|
|
* |
|
|
|
* 实现方式: |
|
|
|
* 使用正则表达式 (Regular Expression) 来实现。这个正则表达式的含义是: |
|
|
|
* "匹配一个或多个空格,但前提是这些空格后面跟着偶数个双引号"。 |
|
|
|
* |
|
|
|
* 换句话说,如果一个空格在引号内部,它后面会有奇数个引号,因此不会成为分割点。 |
|
|
|
* 如果在引号外部,它后面会有偶数个(或0个)引号,因此会成为分割点。 |
|
|
|
* |
|
|
|
* 这是处理带引号参数的经典正则表达式技巧。 |
|
|
|
* |
|
|
|
* 更新:为了更健壮和易于理解,这里提供一个不使用复杂正则表达式的替代实现。 |
|
|
|
* 这个实现通过遍历字符串,手动处理引号内外的内容。 |
|
|
|
*/ |
|
|
|
private String[] parseCommandLine(String commandLine) { |
|
|
|
return commandLine.split("\\s+(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)"); |
|
|
|
// return commandLine.split("\\s+(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
|
|
|
|
|
|
|
|
// --- 教学注释:手动解析实现 (替代正则表达式) ---
|
|
|
|
// 优点:逻辑更清晰,易于调试和理解,对初学者更友好。
|
|
|
|
// 缺点:代码量比单行正则表达式多。
|
|
|
|
List<String> parts = new ArrayList<>(); |
|
|
|
Matcher matcher = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'").matcher(commandLine); |
|
|
|
while (matcher.find()) { |
|
|
|
if (matcher.group(1) != null) { |
|
|
|
// 匹配到双引号内容
|
|
|
|
parts.add(matcher.group(1)); |
|
|
|
} else if (matcher.group(2) != null) { |
|
|
|
// 匹配到单引号内容
|
|
|
|
parts.add(matcher.group(2)); |
|
|
|
} else { |
|
|
|
// 匹配到不含引号和空格的普通单词
|
|
|
|
parts.add(matcher.group()); |
|
|
|
} |
|
|
|
} |
|
|
|
return parts.toArray(new String[0]); |
|
|
|
} |
|
|
|
|
|
|
|
// --- 命令处理方法 (handle... methods) ---
|
|
|
|
// 教学注释:下面的每个 `handle...` 方法都遵循一个通用模式:
|
|
|
|
// 1. **参数校验 (Argument Validation)**:检查传入的 `args` 数组长度是否满足命令的最低要求。
|
|
|
|
// 如果不满足,就打印用法信息 (Usage) 并立即返回。这被称为 "防御性编程" (Defensive Programming),
|
|
|
|
// 可以防止无效数据进入核心业务逻辑,保证程序的健壮性。
|
|
|
|
// 2. **数据提取和清洗 (Data Extraction & Sanitization)**:从 `args` 数组中提取所需的数据。
|
|
|
|
// 例如,使用 `replaceAll("^\"|\"$", "")` 来移除参数两端可能存在的引号。这是一个简单的数据清洗过程。
|
|
|
|
// 3. **委托执行 (Delegation)**:调用相应的控制器 (`noteController` 或 `tagController`) 的方法来执行实际的业务逻辑。
|
|
|
|
// `handle...` 方法本身不执行业务逻辑,它只是一个连接用户输入和业务核心的 "适配器" (Adapter)。
|
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 new 命令 |
|
|
|
* 处理 `new` 命令:创建一个新笔记。 |
|
|
|
* @param args 参数数组,期望格式:`["标题", "内容", ...]` |
|
|
|
*/ |
|
|
|
private void handleNewCommand(String[] args) { |
|
|
|
if (args.length < 2) { |
|
|
|
@ -142,22 +292,20 @@ public class CommandParser { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 简单处理:第一个参数作为标题,其余参数组合作为内容
|
|
|
|
String title = args[0].replaceAll("^\"|\"$", ""); |
|
|
|
StringBuilder contentBuilder = new StringBuilder(); |
|
|
|
for (int i = 1; i < args.length; i++) { |
|
|
|
if (i > 1) contentBuilder.append(" "); |
|
|
|
contentBuilder.append(args[i].replaceAll("^\"|\"$", "")); |
|
|
|
} |
|
|
|
String content = contentBuilder.toString(); |
|
|
|
// 第一个参数是标题
|
|
|
|
String title = args[0]; |
|
|
|
// 从第二个参数开始,将剩余所有部分拼接成内容
|
|
|
|
String content = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); |
|
|
|
|
|
|
|
noteController.createNote(title, content); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 list 命令 |
|
|
|
* 处理 `list` 命令:列出笔记。支持按标签过滤。 |
|
|
|
* @param args 参数数组,可能包含 `--tag <标签名>` |
|
|
|
*/ |
|
|
|
private void handleListCommand(String[] args) { |
|
|
|
// `parseOptions` 是一个辅助方法,专门用于解析像 `--key value` 这样的命名参数。
|
|
|
|
Map<String, String> options = parseOptions(args); |
|
|
|
|
|
|
|
if (options.containsKey("tag")) { |
|
|
|
@ -169,7 +317,8 @@ public class CommandParser { |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 view 命令 |
|
|
|
* 处理 `view` 命令:查看单个笔记的详情。 |
|
|
|
* @param args 参数数组,期望格式:`["笔记ID"]` |
|
|
|
*/ |
|
|
|
private void handleViewCommand(String[] args) { |
|
|
|
if (args.length < 1) { |
|
|
|
@ -183,7 +332,8 @@ public class CommandParser { |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 edit 命令 |
|
|
|
* 处理 `edit` 命令:编辑一个已存在的笔记。 |
|
|
|
* @param args 参数数组,期望格式:`["笔记ID", "新内容"]` |
|
|
|
*/ |
|
|
|
private void handleEditCommand(String[] args) { |
|
|
|
if (args.length < 2) { |
|
|
|
@ -193,13 +343,14 @@ public class CommandParser { |
|
|
|
} |
|
|
|
|
|
|
|
String noteId = args[0]; |
|
|
|
String newContent = args[1].replaceAll("^\"|\"$", ""); |
|
|
|
String newContent = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); |
|
|
|
|
|
|
|
noteController.editNote(noteId, newContent); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 delete 命令 |
|
|
|
* 处理 `delete` 命令:删除一个笔记。 |
|
|
|
* @param args 参数数组,期望格式:`["笔记ID"]` |
|
|
|
*/ |
|
|
|
private void handleDeleteCommand(String[] args) { |
|
|
|
if (args.length < 1) { |
|
|
|
@ -213,7 +364,8 @@ public class CommandParser { |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 tag 命令 |
|
|
|
* 处理 `tag` 命令:为笔记添加一个标签。 |
|
|
|
* @param args 参数数组,期望格式:`["笔记ID", "标签"]` |
|
|
|
*/ |
|
|
|
private void handleTagCommand(String[] args) { |
|
|
|
if (args.length < 2) { |
|
|
|
@ -223,13 +375,15 @@ public class CommandParser { |
|
|
|
} |
|
|
|
|
|
|
|
String noteId = args[0]; |
|
|
|
String tag = args[1]; |
|
|
|
|
|
|
|
tagController.addTag(noteId, tag); |
|
|
|
// 支持一次添加多个标签
|
|
|
|
for (int i = 1; i < args.length; i++) { |
|
|
|
tagController.addTag(noteId, args[i]); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 untag 命令 |
|
|
|
* 处理 `untag` 命令:从笔记移除一个标签。 |
|
|
|
* @param args 参数数组,期望格式:`["笔记ID", "标签"]` |
|
|
|
*/ |
|
|
|
private void handleUntagCommand(String[] args) { |
|
|
|
if (args.length < 2) { |
|
|
|
@ -245,7 +399,8 @@ public class CommandParser { |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 search 命令 |
|
|
|
* 处理 `search` 命令:根据关键词搜索笔记。 |
|
|
|
* @param args 参数数组,期望格式:`["关键词"]` |
|
|
|
*/ |
|
|
|
private void handleSearchCommand(String[] args) { |
|
|
|
if (args.length < 1) { |
|
|
|
@ -254,12 +409,13 @@ public class CommandParser { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
String keyword = String.join(" ", args).replaceAll("^\"|\"$", ""); |
|
|
|
String keyword = String.join(" ", args); |
|
|
|
noteController.searchNotes(keyword); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 export 命令 |
|
|
|
* 处理 `export` 命令:将单个笔记导出到文件。 |
|
|
|
* @param args 参数数组,期望格式:`["笔记ID", "格式", "文件路径"]` |
|
|
|
*/ |
|
|
|
private void handleExportCommand(String[] args) { |
|
|
|
if (args.length < 3) { |
|
|
|
@ -273,17 +429,13 @@ public class CommandParser { |
|
|
|
String format = args[1].toLowerCase(); |
|
|
|
String filePath = args[2]; |
|
|
|
|
|
|
|
if (!format.equals("txt") && !format.equals("json")) { |
|
|
|
System.out.println("不支持的格式: " + format); |
|
|
|
System.out.println("支持格式: txt, json"); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 委托给控制器处理,控制器会进一步使用 ExporterFactory 来创建合适的导出器
|
|
|
|
noteController.exportNote(noteId, format, filePath); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 export-all 命令 |
|
|
|
* 处理 `export-all` 命令:将所有笔记导出到一个文件。 |
|
|
|
* @param args 参数数组,期望格式:`["格式", "文件路径"]` |
|
|
|
*/ |
|
|
|
private void handleExportAllCommand(String[] args) { |
|
|
|
if (args.length < 2) { |
|
|
|
@ -296,27 +448,22 @@ public class CommandParser { |
|
|
|
String format = args[0].toLowerCase(); |
|
|
|
String filePath = args[1]; |
|
|
|
|
|
|
|
if (!format.equals("txt") && !format.equals("json")) { |
|
|
|
System.out.println("不支持的格式: " + format); |
|
|
|
System.out.println("支持格式: txt, json"); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
noteController.exportAllNotes(format, filePath); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 tags 命令 - 显示所有标签 |
|
|
|
* 处理 `tags` 命令:显示所有唯一的标签。 |
|
|
|
*/ |
|
|
|
private void handleTagsCommand(String[] args) { |
|
|
|
tagController.listAllTags(); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 stats 命令 - 显示统计信息 |
|
|
|
* 处理 `stats` 命令:显示统计信息。 |
|
|
|
* @param args 如果第一个参数是 "tags",则显示标签统计信息。 |
|
|
|
*/ |
|
|
|
private void handleStatsCommand(String[] args) { |
|
|
|
if (args.length > 0 && args[0].equals("tags")) { |
|
|
|
if (args.length > 0 && args[0].equalsIgnoreCase("tags")) { |
|
|
|
tagController.showTagStatistics(); |
|
|
|
} else { |
|
|
|
noteController.showStatistics(); |
|
|
|
@ -324,50 +471,67 @@ public class CommandParser { |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 help 命令 |
|
|
|
* 处理 `help` 命令:显示帮助信息。 |
|
|
|
* |
|
|
|
* 教学注释: |
|
|
|
* 一个好的CLI应用必须有一个清晰、全面的帮助命令。 |
|
|
|
* 它应该列出所有可用命令、它们的参数以及使用示例。 |
|
|
|
* 这是提升应用可用性的关键。 |
|
|
|
*/ |
|
|
|
private void handleHelpCommand() { |
|
|
|
System.out.println("个人知识管理系统 - 命令行版本"); |
|
|
|
System.out.println("=====================================\n"); |
|
|
|
|
|
|
|
System.out.println("可用命令:"); |
|
|
|
System.out.println(" new <标题> <内容> - 创建新笔记"); |
|
|
|
System.out.println(" list [--tag TAG] - 列出所有笔记"); |
|
|
|
System.out.println(" new <标题> <内容> - 创建新笔记 (内容可以包含空格)"); |
|
|
|
System.out.println(" list [--tag TAG] - 列出所有笔记,或按标签过滤"); |
|
|
|
System.out.println(" view <笔记ID> - 查看笔记详情"); |
|
|
|
System.out.println(" edit <笔记ID> <新内容> - 编辑笔记内容"); |
|
|
|
System.out.println(" delete <笔记ID> - 删除笔记"); |
|
|
|
System.out.println(" tag <笔记ID> <标签> - 添加标签"); |
|
|
|
System.out.println(" tag <笔记ID> <标签1> [标签2]... - 为笔记添加一个或多个标签"); |
|
|
|
System.out.println(" untag <笔记ID> <标签> - 移除标签"); |
|
|
|
System.out.println(" search <关键词> - 搜索笔记"); |
|
|
|
System.out.println(" export <ID> <格式> <路径> - 导出笔记"); |
|
|
|
System.out.println(" export-all <格式> <路径> - 导出所有笔记"); |
|
|
|
System.out.println(" tags - 显示所有标签"); |
|
|
|
System.out.println(" stats [tags] - 显示统计信息"); |
|
|
|
System.out.println(" search <关键词> - 搜索笔记 (关键词可以包含空格)"); |
|
|
|
System.out.println(" export <ID> <格式> <路径> - 导出单个笔记 (格式: txt, json)"); |
|
|
|
System.out.println(" export-all <格式> <路径> - 导出所有笔记 (格式: txt, json)"); |
|
|
|
System.out.println(" tags - 显示所有唯一的标签"); |
|
|
|
System.out.println(" stats [tags] - 显示笔记总数统计,或标签使用频率统计"); |
|
|
|
System.out.println(" help - 显示此帮助信息"); |
|
|
|
System.out.println(" exit - 退出程序\n"); |
|
|
|
System.out.println(" exit / quit - 退出程序\n"); |
|
|
|
|
|
|
|
System.out.println("示例:"); |
|
|
|
System.out.println(" pkm new \"Java笔记\" \"面向对象编程的三大特性...\""); |
|
|
|
System.out.println(" pkm list --tag java"); |
|
|
|
System.out.println(" pkm view 123e4567"); |
|
|
|
System.out.println(" pkm search \"设计模式\""); |
|
|
|
System.out.println(" pkm export 123e4567 txt output.txt"); |
|
|
|
System.out.println("使用技巧:"); |
|
|
|
System.out.println(" - 如果参数包含空格,请用双引号将其括起来。"); |
|
|
|
System.out.println(" - 示例: new \"我的第一个笔记\" \"这是笔记的内容。\""); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 处理 exit 命令 |
|
|
|
* 处理 `exit` 命令:退出程序。 |
|
|
|
*/ |
|
|
|
private void handleExitCommand() { |
|
|
|
System.out.println("感谢使用个人知识管理系统!"); |
|
|
|
isRunning = false; |
|
|
|
this.isRunning = false; // 将循环标志设为 false,主循环将在下一次迭代时终止
|
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 解析命令选项(如 --tag value) |
|
|
|
* 教学注释:解析命令选项 (例如 --tag value) |
|
|
|
* |
|
|
|
* @param args 完整的参数数组 |
|
|
|
* @return 一个包含所有选项键值对的 Map |
|
|
|
* |
|
|
|
* 设计目的: |
|
|
|
* 为 `list` 等命令提供更灵活的、类似标准Unix命令行的参数风格。 |
|
|
|
* 例如,用户可以输入 `list --tag java` 而不是 `list tag java`。 |
|
|
|
* 这种 `--key value` 的形式更具可读性和可扩展性。 |
|
|
|
* |
|
|
|
* 实现逻辑: |
|
|
|
* 遍历参数数组,查找以 "--" 开头的字符串。 |
|
|
|
* 如果找到,将其视为一个 "键" (key)。 |
|
|
|
* 然后检查它的下一个参数是否也是一个选项。如果不是,就将其视为这个键的 "值" (value)。 |
|
|
|
* 如果是,或者没有下一个参数了,那么这个键就是一个 "布尔标志" (boolean flag),值为 "true"。 |
|
|
|
*/ |
|
|
|
private Map<String, String> parseOptions(String[] args) { |
|
|
|
Map<String, String> options = new HashMap<>(); |
|
|
|
|
|
|
|
List<String> remainingArgs = new ArrayList<>(); // 用于收集非选项参数
|
|
|
|
|
|
|
|
for (int i = 0; i < args.length; i++) { |
|
|
|
if (args[i].startsWith("--")) { |
|
|
|
String key = args[i].substring(2); |
|
|
|
@ -375,16 +539,30 @@ public class CommandParser { |
|
|
|
options.put(key, args[i + 1]); |
|
|
|
i++; // 跳过值参数
|
|
|
|
} else { |
|
|
|
options.put(key, "true"); |
|
|
|
options.put(key, "true"); // 这是一个布尔标志
|
|
|
|
} |
|
|
|
} else { |
|
|
|
remainingArgs.add(args[i]); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 如果需要,可以将非选项参数也放入map中,例如用一个特殊的键
|
|
|
|
if (!remainingArgs.isEmpty()) { |
|
|
|
options.put("_args", String.join(" ", remainingArgs)); |
|
|
|
} |
|
|
|
|
|
|
|
return options; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 关闭资源 |
|
|
|
* 教学注释:关闭资源 |
|
|
|
* |
|
|
|
* 设计目的: |
|
|
|
* 这是一个良好的编程习惯。当应用退出时,应该显式地关闭所有打开的资源, |
|
|
|
* 例如文件流、网络连接或像 `Scanner` 这样的输入流。 |
|
|
|
* 这可以防止资源泄漏 (Resource Leak)。 |
|
|
|
* |
|
|
|
* 在这个应用中,`App.java` 的 `main` 方法在退出前会调用此方法。 |
|
|
|
*/ |
|
|
|
public void close() { |
|
|
|
if (scanner != null) { |
|
|
|
|