Browse Source

增加实体类

master
hewh 4 months ago
parent
commit
2098cae38c
  1. 1
      .gitignore
  2. 47
      README.md
  3. 227
      src/main/java/com/example/model/Note.java
  4. 40
      src/main/java/com/example/service/export/ExportException.java
  5. 39
      src/main/java/com/example/service/export/Exporter.java
  6. 83
      src/main/java/com/example/service/export/ExporterFactory.java
  7. 128
      src/main/java/com/example/service/export/JsonExporter.java
  8. 117
      src/main/java/com/example/service/export/TxtExporter.java
  9. 192
      src/main/java/com/example/service/search/SearchService.java
  10. 343
      src/main/java/com/example/service/storage/JsonStorageService.java
  11. 40
      src/main/java/com/example/service/storage/StorageException.java
  12. 84
      src/main/java/com/example/service/storage/StorageService.java
  13. 158
      src/test/java/com/example/test/EntityTest.java
  14. 8
      test_export.json
  15. 10
      test_export.txt
  16. 14
      test_notes.txt

1
.gitignore

@ -1,2 +1,3 @@
*.class
target/*
target/classes/com/example/App.class

47
README.md

@ -15,17 +15,34 @@ PKM/
│ │ ├── App.java # 主应用程序入口
│ │ ├── cli/
│ │ │ └── CommandParser.java # 命令解析器
│ │ └── model/
│ │ └── ExportFormat.java # 导出格式枚举
│ │ ├── model/
│ │ │ ├── Note.java # 笔记实体类
│ │ │ └── ExportFormat.java # 导出格式枚举
│ │ └── service/
│ │ ├── storage/
│ │ │ ├── StorageService.java # 存储服务接口
│ │ │ ├── StorageException.java # 存储异常类
│ │ │ └── JsonStorageService.java # JSON存储实现
│ │ ├── export/
│ │ │ ├── Exporter.java # 导出器接口
│ │ │ ├── ExportException.java # 导出异常类
│ │ │ ├── ExporterFactory.java # 导出器工厂
│ │ │ ├── TxtExporter.java # TXT导出器
│ │ │ └── JsonExporter.java # JSON导出器
│ │ └── search/
│ │ └── SearchService.java # 搜索服务
│ └── test/
│ └── java/
│ └── com/
│ └── example/
│ └── AppTest.java # 测试类
│ ├── AppTest.java # 应用测试类
│ └── test/
│ └── EntityTest.java # 实体类测试
├── target/classes/ # 编译输出目录
├── pkm.bat # PKM快捷启动脚本
├── run.bat # 运行脚本
├── clean.bat # 清理脚本
├── notes.txt # 笔记数据文件
└── README.md # 项目说明文件
```
@ -131,14 +148,28 @@ pkm.bat help
## 开发状态
当前版本实现了完整的命令行界面和命令解析功能。后续开发计划:
当前版本已完成以下功能:
### ✅ 已实现
- [x] 完整的命令行界面和命令解析功能
- [x] Note实体类(包含完整的属性和方法)
- [x] 存储服务接口和实现(JsonStorageService)
- [x] 导出功能(TXT和JSON格式,使用工厂模式)
- [x] 搜索服务(关键词、标签、模糊搜索等)
- [x] 异常处理机制
- [x] 单元测试和验证
- [ ] 笔记存储和管理
- [ ] 标签系统
- [ ] 搜索功能
- [ ] 导出功能
### 🚧 开发中
- [ ] 将命令解析器与业务服务集成
- [ ] 笔记控制器和标签控制器
- [ ] 配置管理
### 📋 待开发
- [ ] 批处理操作
- [ ] 数据导入功能
- [ ] 更丰富的搜索选项
- [ ] 统计功能(笔记数量、标签云)
## 技术栈
- Java 17
- Maven 3.x

227
src/main/java/com/example/model/Note.java

@ -0,0 +1,227 @@
package com.example.model;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
/**
* 笔记实体类
* 表示系统中的一条笔记记录
*/
public class Note {
private String id; // UUID
private String title; // 笔记标题
private String content; // 笔记内容
private List<String> tags; // 标签列表
private LocalDateTime createdAt; // 创建时间
private LocalDateTime updatedAt; // 更新时间
/**
* 默认构造方法
*/
public Note() {
this.id = UUID.randomUUID().toString();
this.tags = new ArrayList<>();
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
/**
* 带参数的构造方法
*/
public Note(String title, String content) {
this();
this.title = title;
this.content = content;
}
/**
* 完整构造方法
*/
public Note(String id, String title, String content, List<String> tags,
LocalDateTime createdAt, LocalDateTime updatedAt) {
this.id = id;
this.title = title;
this.content = content;
this.tags = tags != null ? new ArrayList<>(tags) : new ArrayList<>();
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
// Getter和Setter方法
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
this.updatedAt = LocalDateTime.now();
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
this.updatedAt = LocalDateTime.now();
}
public List<String> getTags() {
return new ArrayList<>(tags);
}
public void setTags(List<String> tags) {
this.tags = tags != null ? new ArrayList<>(tags) : new ArrayList<>();
this.updatedAt = LocalDateTime.now();
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
// 标签操作方法
/**
* 添加标签
*/
public void addTag(String tag) {
if (tag != null && !tag.trim().isEmpty() && !tags.contains(tag.trim())) {
tags.add(tag.trim());
this.updatedAt = LocalDateTime.now();
}
}
/**
* 移除标签
*/
public boolean removeTag(String tag) {
if (tag != null && tags.remove(tag.trim())) {
this.updatedAt = LocalDateTime.now();
return true;
}
return false;
}
/**
* 检查是否包含指定标签
*/
public boolean hasTag(String tag) {
return tag != null && tags.contains(tag.trim());
}
/**
* 获取标签数量
*/
public int getTagCount() {
return tags.size();
}
/**
* 更新笔记内容
*/
public void update(String newTitle, String newContent) {
if (newTitle != null) {
this.title = newTitle;
}
if (newContent != null) {
this.content = newContent;
}
this.updatedAt = LocalDateTime.now();
}
/**
* 检查笔记是否包含关键词
*/
public boolean containsKeyword(String keyword) {
if (keyword == null || keyword.trim().isEmpty()) {
return false;
}
String lowerKeyword = keyword.toLowerCase();
return (title != null && title.toLowerCase().contains(lowerKeyword)) ||
(content != null && content.toLowerCase().contains(lowerKeyword));
}
/**
* 获取笔记摘要前100个字符
*/
public String getSummary() {
if (content == null || content.isEmpty()) {
return "";
}
String cleanContent = content.replaceAll("\\s+", " ").trim();
if (cleanContent.length() <= 100) {
return cleanContent;
}
return cleanContent.substring(0, 97) + "...";
}
/**
* 复制笔记深拷贝
*/
public Note copy() {
return new Note(
UUID.randomUUID().toString(), // 新的ID
this.title,
this.content,
new ArrayList<>(this.tags),
LocalDateTime.now(), // 新的创建时间
LocalDateTime.now() // 新的更新时间
);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Note note = (Note) obj;
return Objects.equals(id, note.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return String.format("Note{id='%s', title='%s', tags=%s, created=%s, updated=%s}",
id, title, tags, createdAt, updatedAt);
}
/**
* 获取格式化的显示字符串
*/
public String getDisplayString() {
return String.format("[%s] %s (%s) %s",
id.substring(0, 8),
title,
createdAt.toLocalDate(),
tags);
}
}

40
src/main/java/com/example/service/export/ExportException.java

@ -0,0 +1,40 @@
package com.example.service.export;
/**
* 导出操作异常类
* 当导出操作失败时抛出此异常
*/
public class ExportException extends Exception {
/**
* 默认构造方法
*/
public ExportException() {
super();
}
/**
* 带消息的构造方法
* @param message 异常消息
*/
public ExportException(String message) {
super(message);
}
/**
* 带消息和原因的构造方法
* @param message 异常消息
* @param cause 异常原因
*/
public ExportException(String message, Throwable cause) {
super(message, cause);
}
/**
* 带原因的构造方法
* @param cause 异常原因
*/
public ExportException(Throwable cause) {
super(cause);
}
}

39
src/main/java/com/example/service/export/Exporter.java

@ -0,0 +1,39 @@
package com.example.service.export;
import com.example.model.Note;
import java.util.List;
/**
* 导出器接口
* 定义笔记导出功能
*/
public interface Exporter {
/**
* 导出单个笔记
* @param note 要导出的笔记
* @param filePath 导出文件路径
* @throws ExportException 当导出失败时抛出
*/
void export(Note note, String filePath) throws ExportException;
/**
* 导出多个笔记
* @param notes 要导出的笔记列表
* @param filePath 导出文件路径
* @throws ExportException 当导出失败时抛出
*/
void exportAll(List<Note> notes, String filePath) throws ExportException;
/**
* 获取支持的文件扩展名
* @return 文件扩展名 ".txt", ".json"
*/
String getFileExtension();
/**
* 获取导出格式描述
* @return 格式描述
*/
String getFormatDescription();
}

83
src/main/java/com/example/service/export/ExporterFactory.java

@ -0,0 +1,83 @@
package com.example.service.export;
import com.example.model.ExportFormat;
/**
* 导出器工厂类
* 使用工厂模式创建不同格式的导出器
*/
public class ExporterFactory {
/**
* 根据导出格式创建对应的导出器
* @param format 导出格式
* @return 对应的导出器实例
* @throws IllegalArgumentException 当格式不支持时抛出
*/
public static Exporter createExporter(ExportFormat format) {
if (format == null) {
throw new IllegalArgumentException("导出格式不能为空");
}
return switch (format) {
case TXT -> new TxtExporter();
case JSON -> new JsonExporter();
case PDF -> throw new IllegalArgumentException("PDF导出功能暂未实现");
default -> throw new IllegalArgumentException("不支持的导出格式: " + format);
};
}
/**
* 根据格式名称字符串创建导出器
* @param formatName 格式名称 "txt", "json"
* @return 对应的导出器实例
* @throws IllegalArgumentException 当格式不支持时抛出
*/
public static Exporter createExporter(String formatName) {
if (formatName == null || formatName.trim().isEmpty()) {
throw new IllegalArgumentException("导出格式名称不能为空");
}
try {
ExportFormat format = ExportFormat.valueOf(formatName.toUpperCase());
return createExporter(format);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("不支持的导出格式: " + formatName);
}
}
/**
* 获取所有支持的导出格式
* @return 支持的导出格式数组
*/
public static ExportFormat[] getSupportedFormats() {
return new ExportFormat[]{ExportFormat.TXT, ExportFormat.JSON};
}
/**
* 检查格式是否支持
* @param format 要检查的格式
* @return 是否支持
*/
public static boolean isFormatSupported(ExportFormat format) {
return format == ExportFormat.TXT || format == ExportFormat.JSON;
}
/**
* 检查格式名称是否支持
* @param formatName 格式名称
* @return 是否支持
*/
public static boolean isFormatSupported(String formatName) {
if (formatName == null || formatName.trim().isEmpty()) {
return false;
}
try {
ExportFormat format = ExportFormat.valueOf(formatName.toUpperCase());
return isFormatSupported(format);
} catch (IllegalArgumentException e) {
return false;
}
}
}

128
src/main/java/com/example/service/export/JsonExporter.java

@ -0,0 +1,128 @@
package com.example.service.export;
import com.example.model.Note;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* JSON格式导出器
* 将笔记导出为JSON格式简化实现不依赖外部库
*/
public class JsonExporter implements Exporter {
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
@Override
public void export(Note note, String filePath) throws ExportException {
if (note == null) {
throw new ExportException("笔记不能为空");
}
if (filePath == null || filePath.trim().isEmpty()) {
throw new ExportException("文件路径不能为空");
}
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) {
writer.print(noteToJson(note));
} catch (IOException e) {
throw new ExportException("导出JSON文件失败: " + e.getMessage(), e);
}
}
@Override
public void exportAll(List<Note> notes, String filePath) throws ExportException {
if (notes == null) {
throw new ExportException("笔记列表不能为空");
}
if (filePath == null || filePath.trim().isEmpty()) {
throw new ExportException("文件路径不能为空");
}
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) {
writer.println("[");
for (int i = 0; i < notes.size(); i++) {
Note note = notes.get(i);
String json = noteToJson(note);
// 缩进JSON内容
String[] lines = json.split("\n");
for (String line : lines) {
writer.println(" " + line);
}
// 如果不是最后一个元素,添加逗号
if (i < notes.size() - 1) {
writer.println(",");
} else {
writer.println();
}
}
writer.println("]");
} catch (IOException e) {
throw new ExportException("导出JSON文件失败: " + e.getMessage(), e);
}
}
@Override
public String getFileExtension() {
return ".json";
}
@Override
public String getFormatDescription() {
return "JSON格式";
}
/**
* 将笔记对象转换为JSON字符串
*/
private String noteToJson(Note note) {
StringBuilder json = new StringBuilder();
json.append("{\n");
json.append(" \"id\": \"").append(escapeJson(note.getId())).append("\",\n");
json.append(" \"title\": \"").append(escapeJson(note.getTitle())).append("\",\n");
json.append(" \"content\": \"").append(escapeJson(note.getContent())).append("\",\n");
// 标签数组
json.append(" \"tags\": [");
List<String> tags = note.getTags();
for (int i = 0; i < tags.size(); i++) {
json.append("\"").append(escapeJson(tags.get(i))).append("\"");
if (i < tags.size() - 1) {
json.append(", ");
}
}
json.append("],\n");
json.append(" \"createdAt\": \"").append(note.getCreatedAt().format(DATE_FORMATTER)).append("\",\n");
json.append(" \"updatedAt\": \"").append(note.getUpdatedAt().format(DATE_FORMATTER)).append("\"\n");
json.append("}");
return json.toString();
}
/**
* 转义JSON字符串中的特殊字符
*/
private String escapeJson(String input) {
if (input == null) {
return "";
}
return input.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
.replace("\b", "\\b")
.replace("\f", "\\f");
}
}

117
src/main/java/com/example/service/export/TxtExporter.java

@ -0,0 +1,117 @@
package com.example.service.export;
import com.example.model.Note;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* TXT格式导出器
* 将笔记导出为纯文本格式
*/
public class TxtExporter implements Exporter {
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public void export(Note note, String filePath) throws ExportException {
if (note == null) {
throw new ExportException("笔记不能为空");
}
if (filePath == null || filePath.trim().isEmpty()) {
throw new ExportException("文件路径不能为空");
}
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) {
writeNoteToTxt(writer, note);
} catch (IOException e) {
throw new ExportException("导出TXT文件失败: " + e.getMessage(), e);
}
}
@Override
public void exportAll(List<Note> notes, String filePath) throws ExportException {
if (notes == null) {
throw new ExportException("笔记列表不能为空");
}
if (filePath == null || filePath.trim().isEmpty()) {
throw new ExportException("文件路径不能为空");
}
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) {
writer.println("========================================");
writer.println(" 个人知识管理系统");
writer.println(" 笔记导出文件");
writer.println("========================================");
writer.println("导出时间: " + java.time.LocalDateTime.now().format(DATE_FORMATTER));
writer.println("笔记总数: " + notes.size());
writer.println("========================================");
writer.println();
for (int i = 0; i < notes.size(); i++) {
Note note = notes.get(i);
writer.println("========== 笔记 " + (i + 1) + " ==========");
writeNoteToTxt(writer, note);
writer.println();
writer.println("----------------------------------------");
writer.println();
}
writer.println("========================================");
writer.println(" 导出完成");
writer.println("========================================");
} catch (IOException e) {
throw new ExportException("导出TXT文件失败: " + e.getMessage(), e);
}
}
@Override
public String getFileExtension() {
return ".txt";
}
@Override
public String getFormatDescription() {
return "纯文本格式 (TXT)";
}
/**
* 将单个笔记写入TXT格式
*/
private void writeNoteToTxt(PrintWriter writer, Note note) {
writer.println("笔记ID: " + note.getId());
writer.println("标题: " + (note.getTitle() != null ? note.getTitle() : "无标题"));
writer.println("创建时间: " + note.getCreatedAt().format(DATE_FORMATTER));
writer.println("更新时间: " + note.getUpdatedAt().format(DATE_FORMATTER));
// 标签
if (note.getTags().isEmpty()) {
writer.println("标签: 无");
} else {
writer.println("标签: " + String.join(", ", note.getTags()));
}
writer.println();
writer.println("内容:");
writer.println("----------------------------------------");
String content = note.getContent();
if (content == null || content.trim().isEmpty()) {
writer.println("(无内容)");
} else {
// 确保内容正确换行
String[] lines = content.split("\n");
for (String line : lines) {
writer.println(line);
}
}
writer.println("----------------------------------------");
}
}

192
src/main/java/com/example/service/search/SearchService.java

@ -0,0 +1,192 @@
package com.example.service.search;
import com.example.model.Note;
import java.util.List;
import java.util.stream.Collectors;
/**
* 搜索服务类
* 提供笔记搜索功能
*/
public class SearchService {
/**
* 根据关键词搜索笔记
* @param notes 要搜索的笔记列表
* @param keyword 搜索关键词
* @return 匹配的笔记列表
*/
public List<Note> searchByKeyword(List<Note> notes, String keyword) {
if (notes == null || keyword == null || keyword.trim().isEmpty()) {
return List.of();
}
String lowerKeyword = keyword.toLowerCase().trim();
return notes.stream()
.filter(note -> noteContainsKeyword(note, lowerKeyword))
.collect(Collectors.toList());
}
/**
* 根据标签搜索笔记
* @param notes 要搜索的笔记列表
* @param tag 搜索标签
* @return 匹配的笔记列表
*/
public List<Note> searchByTag(List<Note> notes, String tag) {
if (notes == null || tag == null || tag.trim().isEmpty()) {
return List.of();
}
String trimmedTag = tag.trim();
return notes.stream()
.filter(note -> note.getTags().contains(trimmedTag))
.collect(Collectors.toList());
}
/**
* 根据标题搜索笔记
* @param notes 要搜索的笔记列表
* @param title 搜索标题
* @return 匹配的笔记列表
*/
public List<Note> searchByTitle(List<Note> notes, String title) {
if (notes == null || title == null || title.trim().isEmpty()) {
return List.of();
}
String lowerTitle = title.toLowerCase().trim();
return notes.stream()
.filter(note -> note.getTitle() != null &&
note.getTitle().toLowerCase().contains(lowerTitle))
.collect(Collectors.toList());
}
/**
* 根据内容搜索笔记
* @param notes 要搜索的笔记列表
* @param content 搜索内容
* @return 匹配的笔记列表
*/
public List<Note> searchByContent(List<Note> notes, String content) {
if (notes == null || content == null || content.trim().isEmpty()) {
return List.of();
}
String lowerContent = content.toLowerCase().trim();
return notes.stream()
.filter(note -> note.getContent() != null &&
note.getContent().toLowerCase().contains(lowerContent))
.collect(Collectors.toList());
}
/**
* 组合搜索同时匹配关键词和标签
* @param notes 要搜索的笔记列表
* @param keyword 关键词
* @param tag 标签
* @return 匹配的笔记列表
*/
public List<Note> searchByKeywordAndTag(List<Note> notes, String keyword, String tag) {
if (notes == null) {
return List.of();
}
return notes.stream()
.filter(note -> {
boolean keywordMatch = keyword == null || keyword.trim().isEmpty() ||
noteContainsKeyword(note, keyword.toLowerCase().trim());
boolean tagMatch = tag == null || tag.trim().isEmpty() ||
note.getTags().contains(tag.trim());
return keywordMatch && tagMatch;
})
.collect(Collectors.toList());
}
/**
* 模糊搜索在标题内容和标签中搜索
* @param notes 要搜索的笔记列表
* @param query 搜索查询
* @return 匹配的笔记列表
*/
public List<Note> fuzzySearch(List<Note> notes, String query) {
if (notes == null || query == null || query.trim().isEmpty()) {
return List.of();
}
String lowerQuery = query.toLowerCase().trim();
return notes.stream()
.filter(note -> {
// 检查标题
if (note.getTitle() != null &&
note.getTitle().toLowerCase().contains(lowerQuery)) {
return true;
}
// 检查内容
if (note.getContent() != null &&
note.getContent().toLowerCase().contains(lowerQuery)) {
return true;
}
// 检查标签
return note.getTags().stream()
.anyMatch(tag -> tag.toLowerCase().contains(lowerQuery));
})
.collect(Collectors.toList());
}
/**
* 获取包含指定标签的所有笔记
* @param notes 要搜索的笔记列表
* @return 按标签分组的笔记
*/
public List<Note> getNotesWithAnyTag(List<Note> notes) {
if (notes == null) {
return List.of();
}
return notes.stream()
.filter(note -> !note.getTags().isEmpty())
.collect(Collectors.toList());
}
/**
* 获取没有标签的笔记
* @param notes 要搜索的笔记列表
* @return 没有标签的笔记列表
*/
public List<Note> getNotesWithoutTags(List<Note> notes) {
if (notes == null) {
return List.of();
}
return notes.stream()
.filter(note -> note.getTags().isEmpty())
.collect(Collectors.toList());
}
/**
* 检查笔记是否包含关键词在标题或内容中
*/
private boolean noteContainsKeyword(Note note, String lowerKeyword) {
// 检查标题
if (note.getTitle() != null &&
note.getTitle().toLowerCase().contains(lowerKeyword)) {
return true;
}
// 检查内容
if (note.getContent() != null &&
note.getContent().toLowerCase().contains(lowerKeyword)) {
return true;
}
return false;
}
}

343
src/main/java/com/example/service/storage/JsonStorageService.java

@ -0,0 +1,343 @@
package com.example.service.storage;
import com.example.model.Note;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 简化的JSON文件存储服务实现
* 使用简单的文本格式在本地文件系统中存储笔记数据
*/
public class JsonStorageService implements StorageService {
private static final String DEFAULT_FILE_PATH = "notes.txt";
private static final String BACKUP_EXTENSION = ".backup";
private static final String SEPARATOR = "===NOTE_SEPARATOR===";
private final String filePath;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 默认构造方法使用默认文件路径
*/
public JsonStorageService() {
this(DEFAULT_FILE_PATH);
}
/**
* 带文件路径的构造方法
* @param filePath 数据文件路径
*/
public JsonStorageService(String filePath) {
this.filePath = filePath;
// 确保数据目录存在
createDataDirectoryIfNotExists();
// 如果文件不存在,创建空文件
if (!fileExists()) {
try {
saveAll(new ArrayList<>());
} catch (StorageException e) {
System.err.println("初始化存储文件失败: " + e.getMessage());
}
}
}
@Override
public void saveNote(Note note) throws StorageException {
if (note == null) {
throw new StorageException("笔记不能为空");
}
lock.writeLock().lock();
try {
List<Note> notes = findAllNotesInternal();
// 查找是否已存在相同ID的笔记
boolean found = false;
for (int i = 0; i < notes.size(); i++) {
if (notes.get(i).getId().equals(note.getId())) {
notes.set(i, note);
found = true;
break;
}
}
// 如果不存在,添加新笔记
if (!found) {
notes.add(note);
}
saveAllInternal(notes);
} finally {
lock.writeLock().unlock();
}
}
@Override
public boolean deleteNote(String id) throws StorageException {
if (id == null || id.trim().isEmpty()) {
throw new StorageException("笔记ID不能为空");
}
lock.writeLock().lock();
try {
List<Note> notes = findAllNotesInternal();
boolean removed = notes.removeIf(note -> note.getId().equals(id));
if (removed) {
saveAllInternal(notes);
}
return removed;
} finally {
lock.writeLock().unlock();
}
}
@Override
public Optional<Note> findNoteById(String id) throws StorageException {
if (id == null || id.trim().isEmpty()) {
return Optional.empty();
}
lock.readLock().lock();
try {
List<Note> notes = findAllNotesInternal();
return notes.stream()
.filter(note -> note.getId().equals(id))
.findFirst();
} finally {
lock.readLock().unlock();
}
}
@Override
public List<Note> findAllNotes() throws StorageException {
lock.readLock().lock();
try {
return findAllNotesInternal();
} finally {
lock.readLock().unlock();
}
}
@Override
public void saveAll(List<Note> notes) throws StorageException {
lock.writeLock().lock();
try {
saveAllInternal(notes);
} finally {
lock.writeLock().unlock();
}
}
@Override
public boolean noteExists(String id) throws StorageException {
return findNoteById(id).isPresent();
}
@Override
public long getNotesCount() throws StorageException {
return findAllNotes().size();
}
@Override
public void clearAll() throws StorageException {
saveAll(new ArrayList<>());
}
@Override
public void backup(String backupPath) throws StorageException {
if (backupPath == null || backupPath.trim().isEmpty()) {
// 使用默认备份路径
LocalDateTime now = LocalDateTime.now();
String timestamp = now.format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
backupPath = filePath + "." + timestamp + BACKUP_EXTENSION;
}
try {
Path source = Paths.get(filePath);
Path target = Paths.get(backupPath);
if (Files.exists(source)) {
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
} else {
throw new StorageException("源文件不存在: " + filePath);
}
} catch (IOException e) {
throw new StorageException("备份失败: " + e.getMessage(), e);
}
}
@Override
public void restore(String backupPath) throws StorageException {
if (backupPath == null || backupPath.trim().isEmpty()) {
throw new StorageException("备份文件路径不能为空");
}
try {
Path source = Paths.get(backupPath);
Path target = Paths.get(filePath);
if (Files.exists(source)) {
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
} else {
throw new StorageException("备份文件不存在: " + backupPath);
}
} catch (IOException e) {
throw new StorageException("恢复失败: " + e.getMessage(), e);
}
}
/**
* 内部方法读取所有笔记不加锁
*/
private List<Note> findAllNotesInternal() throws StorageException {
try {
File file = new File(filePath);
if (!file.exists() || file.length() == 0) {
return new ArrayList<>();
}
List<Note> notes = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
Note currentNote = null;
StringBuilder contentBuilder = new StringBuilder();
String field = null;
while ((line = reader.readLine()) != null) {
if (line.equals(SEPARATOR)) {
if (currentNote != null) {
if ("CONTENT".equals(field)) {
currentNote.setContent(contentBuilder.toString().trim());
}
notes.add(currentNote);
}
currentNote = new Note();
contentBuilder.setLength(0);
field = null;
} else if (line.startsWith("ID:")) {
if (currentNote != null) {
currentNote.setId(line.substring(3).trim());
}
field = "ID";
} else if (line.startsWith("TITLE:")) {
if (currentNote != null) {
currentNote.setTitle(line.substring(6).trim());
}
field = "TITLE";
} else if (line.startsWith("TAGS:")) {
if (currentNote != null) {
String tagsStr = line.substring(5).trim();
if (!tagsStr.isEmpty()) {
String[] tagArray = tagsStr.split(",");
for (String tag : tagArray) {
currentNote.addTag(tag.trim());
}
}
}
field = "TAGS";
} else if (line.startsWith("CREATED:")) {
if (currentNote != null) {
try {
currentNote.setCreatedAt(LocalDateTime.parse(line.substring(8).trim()));
} catch (Exception e) {
currentNote.setCreatedAt(LocalDateTime.now());
}
}
field = "CREATED";
} else if (line.startsWith("UPDATED:")) {
if (currentNote != null) {
try {
currentNote.setUpdatedAt(LocalDateTime.parse(line.substring(8).trim()));
} catch (Exception e) {
currentNote.setUpdatedAt(LocalDateTime.now());
}
}
field = "UPDATED";
} else if (line.startsWith("CONTENT:")) {
field = "CONTENT";
contentBuilder.append(line.substring(8));
if (line.length() > 8) {
contentBuilder.append("\n");
}
} else if ("CONTENT".equals(field)) {
contentBuilder.append(line).append("\n");
}
}
// 处理最后一个笔记
if (currentNote != null) {
if ("CONTENT".equals(field)) {
currentNote.setContent(contentBuilder.toString().trim());
}
notes.add(currentNote);
}
}
return notes;
} catch (IOException e) {
throw new StorageException("读取笔记数据失败: " + e.getMessage(), e);
}
}
/**
* 内部方法保存所有笔记不加锁
*/
private void saveAllInternal(List<Note> notes) throws StorageException {
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) {
if (notes != null) {
for (Note note : notes) {
writer.println("ID:" + note.getId());
writer.println("TITLE:" + (note.getTitle() != null ? note.getTitle() : ""));
writer.println("TAGS:" + String.join(",", note.getTags()));
writer.println("CREATED:" + note.getCreatedAt());
writer.println("UPDATED:" + note.getUpdatedAt());
writer.println("CONTENT:" + (note.getContent() != null ? note.getContent() : ""));
writer.println(SEPARATOR);
}
}
} catch (IOException e) {
throw new StorageException("保存笔记数据失败: " + e.getMessage(), e);
}
}
/**
* 检查文件是否存在
*/
private boolean fileExists() {
return new File(filePath).exists();
}
/**
* 创建数据目录如果不存在
*/
private void createDataDirectoryIfNotExists() {
File file = new File(filePath);
File parentDir = file.getParentFile();
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs();
}
}
/**
* 获取文件路径
*/
public String getFilePath() {
return filePath;
}
}

40
src/main/java/com/example/service/storage/StorageException.java

@ -0,0 +1,40 @@
package com.example.service.storage;
/**
* 存储操作异常类
* 当存储操作失败时抛出此异常
*/
public class StorageException extends Exception {
/**
* 默认构造方法
*/
public StorageException() {
super();
}
/**
* 带消息的构造方法
* @param message 异常消息
*/
public StorageException(String message) {
super(message);
}
/**
* 带消息和原因的构造方法
* @param message 异常消息
* @param cause 异常原因
*/
public StorageException(String message, Throwable cause) {
super(message, cause);
}
/**
* 带原因的构造方法
* @param cause 异常原因
*/
public StorageException(Throwable cause) {
super(cause);
}
}

84
src/main/java/com/example/service/storage/StorageService.java

@ -0,0 +1,84 @@
package com.example.service.storage;
import com.example.model.Note;
import java.util.List;
import java.util.Optional;
/**
* 存储服务接口
* 定义笔记数据的持久化操作
*/
public interface StorageService {
/**
* 保存笔记
* @param note 要保存的笔记
* @throws StorageException 当保存失败时抛出
*/
void saveNote(Note note) throws StorageException;
/**
* 根据ID删除笔记
* @param id 笔记ID
* @return 是否删除成功
* @throws StorageException 当删除失败时抛出
*/
boolean deleteNote(String id) throws StorageException;
/**
* 根据ID查找笔记
* @param id 笔记ID
* @return 找到的笔记如果不存在则返回Optional.empty()
* @throws StorageException 当查找失败时抛出
*/
Optional<Note> findNoteById(String id) throws StorageException;
/**
* 查找所有笔记
* @return 所有笔记的列表
* @throws StorageException 当查找失败时抛出
*/
List<Note> findAllNotes() throws StorageException;
/**
* 批量保存笔记
* @param notes 要保存的笔记列表
* @throws StorageException 当保存失败时抛出
*/
void saveAll(List<Note> notes) throws StorageException;
/**
* 检查笔记是否存在
* @param id 笔记ID
* @return 笔记是否存在
* @throws StorageException 当检查失败时抛出
*/
boolean noteExists(String id) throws StorageException;
/**
* 获取笔记总数
* @return 笔记总数
* @throws StorageException 当获取失败时抛出
*/
long getNotesCount() throws StorageException;
/**
* 清空所有笔记
* @throws StorageException 当清空失败时抛出
*/
void clearAll() throws StorageException;
/**
* 备份数据
* @param backupPath 备份文件路径
* @throws StorageException 当备份失败时抛出
*/
void backup(String backupPath) throws StorageException;
/**
* 从备份恢复数据
* @param backupPath 备份文件路径
* @throws StorageException 当恢复失败时抛出
*/
void restore(String backupPath) throws StorageException;
}

158
src/test/java/com/example/test/EntityTest.java

@ -0,0 +1,158 @@
package com.example.test;
import com.example.model.Note;
import com.example.model.ExportFormat;
import com.example.service.storage.JsonStorageService;
import com.example.service.storage.StorageException;
import com.example.service.export.ExporterFactory;
import com.example.service.export.Exporter;
import com.example.service.export.ExportException;
import com.example.service.search.SearchService;
import java.util.List;
/**
* 实体类测试程序
* 验证各个实体类和服务类的基本功能
*/
public class EntityTest {
public static void main(String[] args) {
System.out.println("========================================");
System.out.println(" 个人知识管理系统实体类测试");
System.out.println("========================================\n");
try {
testNoteEntity();
testStorageService();
testExportService();
testSearchService();
System.out.println("✅ 所有测试通过!");
} catch (Exception e) {
System.err.println("❌ 测试失败: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 测试Note实体类
*/
private static void testNoteEntity() {
System.out.println("🧪 测试Note实体类...");
// 创建笔记
Note note = new Note("Java学习笔记", "面向对象编程的三大特性:封装、继承、多态");
note.addTag("编程");
note.addTag("Java");
note.addTag("学习");
System.out.println(" 创建笔记: " + note.getTitle());
System.out.println(" 笔记ID: " + note.getId());
System.out.println(" 标签数量: " + note.getTagCount());
System.out.println(" 包含关键词'面向对象': " + note.containsKeyword("面向对象"));
System.out.println(" 摘要: " + note.getSummary());
// 测试标签操作
note.removeTag("学习");
System.out.println(" 移除标签后数量: " + note.getTagCount());
System.out.println("✅ Note实体类测试通过\n");
}
/**
* 测试存储服务
*/
private static void testStorageService() throws StorageException {
System.out.println("🧪 测试存储服务...");
JsonStorageService storage = new JsonStorageService("test_notes.txt");
// 清空现有数据
storage.clearAll();
// 创建测试笔记
Note note1 = new Note("设计模式", "单例模式确保一个类只有一个实例");
note1.addTag("编程");
note1.addTag("设计模式");
Note note2 = new Note("数据结构", "栈是后进先出的数据结构");
note2.addTag("编程");
note2.addTag("算法");
// 保存笔记
storage.saveNote(note1);
storage.saveNote(note2);
System.out.println(" 保存了2条笔记");
System.out.println(" 笔记总数: " + storage.getNotesCount());
// 查找笔记
List<Note> allNotes = storage.findAllNotes();
System.out.println(" 读取到 " + allNotes.size() + " 条笔记");
// 查找单个笔记
var foundNote = storage.findNoteById(note1.getId());
System.out.println(" 根据ID查找笔记: " + (foundNote.isPresent() ? "成功" : "失败"));
System.out.println("✅ 存储服务测试通过\n");
}
/**
* 测试导出服务
*/
private static void testExportService() throws StorageException, ExportException {
System.out.println("🧪 测试导出服务...");
// 创建测试笔记
Note note = new Note("测试导出", "这是一个用于测试导出功能的笔记");
note.addTag("测试");
note.addTag("导出");
// 测试TXT导出
Exporter txtExporter = ExporterFactory.createExporter(ExportFormat.TXT);
txtExporter.export(note, "test_export.txt");
System.out.println(" TXT导出: " + txtExporter.getFormatDescription());
// 测试JSON导出
Exporter jsonExporter = ExporterFactory.createExporter(ExportFormat.JSON);
jsonExporter.export(note, "test_export.json");
System.out.println(" JSON导出: " + jsonExporter.getFormatDescription());
// 测试工厂方法
System.out.println(" 支持的格式: " + ExporterFactory.getSupportedFormats().length + " 种");
System.out.println(" TXT格式支持: " + ExporterFactory.isFormatSupported("txt"));
System.out.println("✅ 导出服务测试通过\n");
}
/**
* 测试搜索服务
*/
private static void testSearchService() throws StorageException {
System.out.println("🧪 测试搜索服务...");
JsonStorageService storage = new JsonStorageService("test_notes.txt");
SearchService searchService = new SearchService();
List<Note> allNotes = storage.findAllNotes();
// 按关键词搜索
List<Note> keywordResults = searchService.searchByKeyword(allNotes, "设计");
System.out.println(" 关键词'设计'搜索结果: " + keywordResults.size() + " 条");
// 按标签搜索
List<Note> tagResults = searchService.searchByTag(allNotes, "编程");
System.out.println(" 标签'编程'搜索结果: " + tagResults.size() + " 条");
// 模糊搜索
List<Note> fuzzyResults = searchService.fuzzySearch(allNotes, "模式");
System.out.println(" 模糊搜索'模式'结果: " + fuzzyResults.size() + " 条");
// 无标签笔记
List<Note> untaggedNotes = searchService.getNotesWithoutTags(allNotes);
System.out.println(" 无标签笔记: " + untaggedNotes.size() + " 条");
System.out.println("✅ 搜索服务测试通过\n");
}
}

8
test_export.json

@ -0,0 +1,8 @@
{
"id": "35e2aab6-dba7-4b78-ae4d-cb123d44059b",
"title": "测试导出",
"content": "这是一个用于测试导出功能的笔记",
"tags": ["测试", "导出"],
"createdAt": "2025-07-13T23:13:51",
"updatedAt": "2025-07-13T23:13:51"
}

10
test_export.txt

@ -0,0 +1,10 @@
笔记ID: 35e2aab6-dba7-4b78-ae4d-cb123d44059b
标题: 测试导出
创建时间: 2025-07-13 23:13:51
更新时间: 2025-07-13 23:13:51
标签: 测试, 导出
内容:
----------------------------------------
这是一个用于测试导出功能的笔记
----------------------------------------

14
test_notes.txt

@ -0,0 +1,14 @@
ID:960b330f-f7d5-4c31-b0fe-ce48d81ae4ed
TITLE:
TAGS:
CREATED:2025-07-13T23:13:51.844655200
UPDATED:2025-07-13T23:13:51.844655200
CONTENT:
===NOTE_SEPARATOR===
ID:60cf8fb5-05af-4501-bce2-2b2da1cd7219
TITLE:数据结构
TAGS:编程,算法
CREATED:2025-07-13T23:13:51.836669200
UPDATED:2025-07-13T23:13:51.836669200
CONTENT:栈是后进先出的数据结构
===NOTE_SEPARATOR===
Loading…
Cancel
Save