目录
项目总览
项目流程
项目搭建
maven项目
类的设计和实现
Main类
resources包
app.fxml文件
init.sql文件
工具包Util
Util类
PinyinUtil类
DBUtil类
DBInit类
app包
Controller类
FileMeta类
task包
FileScanner类
FileSearch类
Callback包
FileScannerCallback接口
FileSave2DB类
项目测试用例设计
项目展示
项目名称:Search-Everything文件搜索工具
项目简介:仿照Everything实现的文件搜索工具,支持跨平台的使用,如:Windows,Linux操作系统
项目功能:
选择文件夹路径后,多线程扫描该文件夹下的子文件,展示子文件的名称,大小,修改时间
支持基于文件名、部分文件名、文件名拼音或拼音首字母的搜索
项目环境:Windows,IDEA,Maven,Java8
技术栈: JavaFX,多线程,IO流,SQLite数据库
创建一个基于jdk8的maven项目search_everything,并引入三个jar包:
pinyin4j : 汉语拼音的处理工具,提供对中文汉字到拼音的转换。存在多音字的情况,根据一个字符可以获取多个字符串
sqlite-jdbc:SQLite数据库
lombok库:普通的Java类,使用这个lombok库可以直接通过几个注解,注入getter, setter, tostring,hashCode, equals等等方法的代码
maven是一个项目管理工具,方便进行第三方jar包的导入和管理,方便对当时项目的整个生命周期(打包、测试、发布等)进行追踪
jar包其实就是一个压缩包,jar包中是一系列编译好的class文件。
4.0.0
org.example
search_everything
1.0-SNAPSHOT
8
8
com.belerweb
pinyin4j
2.5.1
org.xerial
sqlite-jdbc
3.36.0.3
org.projectlombok
lombok
1.18.24
org.apache.maven.plugins
maven-compiler-plugin
1.8
1.8
UTF-8
org.apache.maven.plugins
maven-jar-plugin
Main
true
lib/
org.apache.maven.plugins
maven-dependency-plugin
2.8
copy
package
copy-dependencies
${project.build.directory}/lib
安装Database Navigator插件,用于查看数据库是否连接成功,数据是否加载成功
使用方法:
作用:提供项目入口
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
/**
* 主方法,项目入口
**/
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getClassLoader().getResource("app.fxml"));
primaryStage.setTitle("search_everything");
primaryStage.setScene(new Scene(root, 1000, 800));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
作用:项目界面的JavaFX脚本
界面样式:
作用:存储创建数据库表的sql语句
drop table if exists file_meta;
create table if not exists file_meta(
name varchar(50) not null,
path varchar(100) not null,
is_directory boolean not null,
size bigint,
last_modified timestamp not null,
pinyin varchar(200),
pinyin_first varchar(50)
);
作用:通用工具类,提供一些通用的工具方法
package util;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Util {
public static final String DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* 根据传入的文件大小返回不同的单位
* @param size
* @return
*/
public static String parseSize(Long size) {
// 数组中单位由小到大逐渐增大,相邻两单位之间差1024
String[] unit = {"B","KB","MB","GB"};
int flag = 0;
while (size > 1024) {
size /= 1024;
flag ++;
}
return size + unit[flag];
}
public static String parseFileType(Boolean directory) {
// directory为true就是文件夹,否则是文件
return directory ? "文件夹" : "文件";
}
public static String parseDate(Date lastModified) {
return new SimpleDateFormat(DATE_PATTERN).format(lastModified);
}
}
作用:将中文字符串转化为字母字符串(拼音全拼和拼音首字母 )。
例:工具包 ==> 全拼:gongjvbao, 拼音首字母:gjb
package util;
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.HanyuPinyinVCharType;
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;
import java.util.Arrays;
public class PinyinUtil {
// 定义汉语拼音的配置FORMAT对象 全局常量,必须在定义时初始化,全局唯一
// 这个对象将配置一些汉字字符转为拼音字符串时的规则
private static final HanyuPinyinOutputFormat FORMAT;
// 所有的中文对应的unicode编码的区间
private static final String CHINESE_PATTERN = "[\\u4E00-\\u9FA5]";
static{
FORMAT = new HanyuPinyinOutputFormat();
// 设置转化出的英文字符串为全小写
FORMAT.setCaseType(HanyuPinyinCaseType.LOWERCASE);
// 设置转化出的英文字符串不使用音调
FORMAT.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
// 设置用英文字符v代替拼音中的yv
FORMAT.setVCharType(HanyuPinyinVCharType.WITH_V);
}
/**
* 传入任意的文件名称,就能将该文件名称转为字母字符串全拼和首字母小写字符串
* 碰见汉字转化为拼音,碰见非汉字直接存入结果
* @param name
* @return
*/
public static String[] getPinyinByFileName(String name){
// 存放转化结果,数组中第一个元素为文件名的全拼,第二个元素为文件名拼音的首字母
String[] res = new String[2];
StringBuilder allNameAppender = new StringBuilder();
StringBuilder firstCaseAppender = new StringBuilder();
// 遍历字符串开始转换
for (char c : name.toCharArray()) {
try {
// PinyinHelper.toHanyuPinyinStringArray方法:遇见非中文字符都转为空数组[]
// 遇见汉字转为包含这个汉字的所有读音的字符串数组,如 ‘和’ --> [he, he, huo, huo, huo, hai, he, hu]
// 此处我们不考虑多音字,直接取第一个读音使用
String[] pinyinStrings = PinyinHelper.toHanyuPinyinStringArray(c,PinyinUtil.FORMAT);
if(pinyinStrings==null || pinyinStrings.length==0){
// 当遇见非中文字符时,直接加入结果集
allNameAppender.append(c);
firstCaseAppender.append(c);
}else {
// 遇见中文字符,取多音字的第一个音
allNameAppender.append(pinyinStrings[0]);
firstCaseAppender.append(pinyinStrings[0].charAt(0));
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
// 碰到非中文字符,直接保留
allNameAppender.append(c);
firstCaseAppender.append(c);
}
}
res[0] = allNameAppender.toString();
res[1] = firstCaseAppender.toString();
return res;
}
/**
* 判断给定的字符串中是否包含中文字符
* @param str
* @return
*/
public static boolean containsChinese(String str){
// 用正则表达式判断str是否包含中文字符
return str.matches(".*"+CHINESE_PATTERN+".*");
}
}
作用:用于创建SQLite数据源以及实现数据库的连接和关闭。
选择SQLite嵌入式数据库,是因为我们的search_everything项目是一个小型项目,没有必要选择过大的数据库。对于SQLite数据库来说,它就是一个文件,没有服务端和客户端,体量较小,适合当前项目。
package util;
import org.sqlite.SQLiteConfig;
import org.sqlite.SQLiteDataSource;
import javax.sql.DataSource;
import java.io.File;
import java.sql.*;
public class DBUtil {
// 单例数据源
private volatile static DataSource DATASOURCE;
// 单例数据连接
private volatile static Connection CONNECTION;
// 获取数据源方法,使用double-check单例模式获取数据源对象
private static DataSource getDataSource() {
if (DATASOURCE == null) {
// 多线程场景下,只有一个线程能进入同步代码块
synchronized (DBUtil.class) {
// 防止其他线程释放锁后,再次进入同步代码块又创建了对象
if (DATASOURCE == null) {
// SQLite没有账户密码,只需要配置日期格式即可
// 默认时间格式是时间戳,为了方便阅读,我们自定义一个时间格式
SQLiteConfig config = new SQLiteConfig();
config.setDateStringFormat(Util.DATE_PATTERN);
DATASOURCE = new SQLiteDataSource(config);
// 配置数据源的URL是SQLite子类独有的方法,因此向下转型
((SQLiteDataSource) DATASOURCE).setUrl(getUrl());
}
}
}
return DATASOURCE;
}
/**
* 配置SQLite数据库的地址
* @return
*/
private static String getUrl() {
String path = "E:\\JavaStudy\\BiteSchool\\JavaProject\\search_everything\\target";
// separator是File类的常量,用于替换文件分隔符
// 设置原因:由于不同操作系统的文件分隔符不同,win下的程序到Linux系统下可能就无法使用
String url = "jdbc:sqlite://" + path + File.separator + "search_everything.db";
System.out.println("获取数据库的连接为 : " + url);
return url;
}
/**
* 获取连接。多线程场景下SQLite要求多个线程使用同一个连接进行处理
* @return
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
if(CONNECTION == null){
synchronized (DBUtil.class){
if(CONNECTION == null){
CONNECTION = getDataSource().getConnection();
}
}
}
return CONNECTION;
}
/**
* 关闭数据库资源
* @param statement
*/
public static void close(Statement statement) {
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
public static void close(PreparedStatement ps, ResultSet rs) {
close(ps);
if(rs != null){
try {
rs.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
作用:读取resources包下的init.sql文件,根据读取到的sql语句在界面初始化时创建数据表
package util;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.sql.SQLException;
public class DBInit {
public static List readSQL() {
List ret = new ArrayList<>();
// 从init.sql文件中获取内容,需要拿到文件的输入流
try {
// 采用类加载器的方式引入资源文件
// JVM在加载类的时候用到的ClassLoader类
InputStream in = DBUtil.class.getClassLoader().
getResourceAsStream("init.sql");
// 从文件获取输入流
Scanner scanner = new Scanner(in);
// 自定义分隔符,一个sql语句的结束标志
scanner.useDelimiter(";");
// nextLine默认碰到换行\n分隔
// next方法按照自定义的分隔符拆分
while (scanner.hasNext()) {
String str = scanner.next();
if ("".equals(str) || "\n".equals(str)){
continue;
}
if (str.contains("--")) {
str = str.replaceAll("--","");
}
ret.add(str);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
System.out.println("读取到的SQL内容为:");
System.out.println(ret);
return ret;
}
/**
* 在界面初始化时先初始化数据库,创建数据表
*/
public static void init() {
Connection connection = null;
Statement statement = null;
try {
connection = DBUtil.getConnection();
// 获取要执行的sql语句
List sqls = readSQL();
// 采用了普通的Statement接口,没有用PrepareStatement是因为
// PrepareStatement一次只能执行一行sql语句
statement = connection.createStatement();
for (String sql : sqls) {
System.out.println("执行SQL操作 : " + sql);
statement.executeUpdate(sql);
}
}catch (SQLException e) {
System.err.println("数据库初始化失败");
e.printStackTrace();
}finally {
DBUtil.close(statement);
}
}
}
注意:若使用相对路径获取init.sql文件,将项目打包成jar包后找不到该文件
InputStream in = new FileInputStream("src/main/resources/init.sql");
原因:
src下java包内的所有文件打包后被编译到target目录下的classes文件夹中。编译后resource内的所有文件被放到target下的classes文件夹中。
解决方法:
使用反射找到类需要用到的文件。用类名.class.getClassLoader().getResourceAsStream(文件名)获取到编译后的classes目录下的指定文件
InputStream in = DBInit.class.getClassLoader().getResourceAsStream("init.sql");
作用:和界面文件app.fxml搭配的类,app.fxml中的所有数据提交给此类来执行后序流程,然后controller再将数据写回界面
app.fxml中fx:id的名称要和app.Controller类中的属性名称完全一致,这样界面中的内容才会正确的被Controller类所接收。
package app;
import callback.impl.FileSave2DB;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.DirectoryChooser;
import javafx.stage.Window;
import task.FileScanner;
import task.FileSearch;
import util.DBInit;
import java.io.File;
import java.net.URL;
import java.util.List;
import java.util.ResourceBundle;
public class Controller implements Initializable {
@FXML
private GridPane rootPane;
@FXML
private TextField searchField; // id为searchField的搜索框标签
@FXML
private TableView fileTable;
@FXML
private Label srcDirectory; // 引入标签
private List fileMetas; // 存储扫描到的文件信息
private Thread scanThread;
/**
* 点击运行项目,界面初始化时加载的方法
* @param location
* @param resources
*/
public void initialize(URL location, ResourceBundle resources) {
// 界面初始化的同时加载数据库
DBInit.init();
// 添加搜索框监听器,内容改变时执行监听事件
searchField.textProperty().addListener(new ChangeListener() {
public void changed(ObservableValue extends String> observable, String oldValue, String newValue) {
freshTable();
}
});
}
/**
* 选择路径后触发的方法,用于获取选择的目录路径
* @param event
*/
public void choose(Event event) {
// 选择文件目录
DirectoryChooser directoryChooser=new DirectoryChooser();
Window window = rootPane.getScene().getWindow();
File file = directoryChooser.showDialog(window);
if(file == null)
return;
// 获取选择的目录路径,并显示
String path = file.getPath();
// 在界面中显示路径
this.srcDirectory.setText(path); // 给标签设置内容
// 获取要扫描的文件夹路径之后,进行文件的扫描工作
System.out.println("开始进行文件扫描任务,根路径为 : " + path);
long start = System.nanoTime();
// 在触发程序进行文件扫描任务时,就要决定将信息保存到哪个终端,本项目选择存储到数据库
FileScanner fileScanner = new FileScanner(new FileSave2DB());
// 中断操作
if(scanThread != null){
// 已经创建过一个扫描刷新界面任务A,但由于目标路径A下内容过大,该任务A还没执行结束
// 中断该任务A,执行当前文件目录下的扫描刷新任务B
scanThread.interrupt();
fileTable.getItems().clear();
}
// 开启新线程,用于扫描新选择的目录,并刷新界面
scanThread = new Thread(()->{
fileScanner.scan(file);
// 刷新界面,展示刚才扫描到的文件信息
freshTable();
});
scanThread.start();
}
// 刷新表格数据
private void freshTable(){
ObservableList metas = fileTable.getItems();
metas.clear();
// 获取id为srcDirectory标签的文本,也就是当前所在的路径
String dir = srcDirectory.getText();
// 路径不为空,即界面中已经选择了文件
if(dir != null && dir.trim().length() != 0){
// 此时已经将最新的数据保存到了数据库中,我们只需要按要求取出数据库中的内容展示到界面上即可
// 判断搜索框中是否有用户填写的内容(用户是否在当前路径下做搜索操作)
String content = searchField.getText(); // 获取搜索框中文本内容
// 若为空,相当于展示数据库存储的所有内容,若不为空展示数据空中满足条件的内容
List fileFromDB = FileSearch.search(dir,content);
// 将查询结果,即fileFromDB中的所有内容展示到界面上去
metas.addAll(fileFromDB);
}
}
}
作用:使用此类完成和数据库数据表的映射,关系该类的一个对象就对应数据表的一行数据。数据表的所有内容就是一个FileMeta类的对象数组。
功能:
设置file_meta表中必备的属性值(通过有参构造方法)
改写一些特殊属性的显示方式,如IsDirectory不再只显示fase或true,直接显示文件或文件夹(借助Util类里定义的转化方法)
package app;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import util.Util;
import java.util.Date;
/**
* 与数据库file_meta表对应
*/
@Data
@NoArgsConstructor
@ToString
@EqualsAndHashCode
public class FileMeta {
//create table if not exists file_meta(
// name varchar(50) not null,
// path varchar(100) not null,
// is_directory boolean not null,
// size bigint not null,
// last_modified timestamp not null,
// pinyin varchar(200),
// pinyin_first varchar(50)
// );
// 在数据表到java类的映射中基本类型统一使用包装类
// 必备属性(构造方法中必存在)
private String name;
private String path;
private Boolean isDirectory;
private Long size;
private Date lastModified;
// 可选属性(构造方法中可以不存在)
// 若包含中文名称,名称全拼
private String pinYin;
// 拼音首字母
private String pinYinFirst;
// 以下三个属性在界面中展示时,需要将当前属性值做处理之后展示
// 注意,这些属性名要和app.fxml中保持一致
// 文件类型
private String isDirectoryText;
// 文件大小
private String sizeText;
// 上次修改时间
private String lastModifiedText;
public void setSize(Long size) {
this.size = size;
this.sizeText = Util.parseSize(size);
}
public void setIsDirectory(Boolean directory) {
isDirectory = directory;
this.isDirectoryText = Util.parseFileType(directory);
}
public void setLastModified(Date lastModified) {
this.lastModified = lastModified;
this.lastModifiedText = Util.parseDate(lastModified);
}
public FileMeta(String name, String path, Boolean isDirectory, long size, Date lastModified) {
this.name = name;
this.path = path;
this.isDirectory = isDirectory;
this.size = size;
this.lastModified = lastModified;
}
}
作用:文件信息扫描任务类,递归的访问磁盘中指定文件夹的所有文件,并保存到SQLite数据库。使用回调函数思想。
回调函数是一种程序设计的思想,将两个独立的功能拆分为不同的方法,解耦,但是这两个方法又是互相配合完成同一个功能。其实就是在一个类中调用另一个类的方法来辅助解决问题。
思路:
scan函数就是在指定的文件夹中扫描所有的子文件和子文件夹 - 功能1 callback函数(见Callback包)就是将扫描到的文件夹信息保存到SQLite数据库终端中 ==> 将当前路径下的所有文件信息保存到数据库中 - 功能2
功能1和功能2搭配起来,就实现了将指定目录下的所有文件和文件夹扫描出来后保存到数据库中
实现流程:
每当碰到一个文件夹,新建一个任务(线程),子线程个数+1 a.先把当前目录下的所有文件夹和文件信息保存到数据库中(调用callback函数) b.遍历当前目录下的所有文件和文件夹 若碰到文件,只是进行文件个数的累加 若碰到文件夹,调用递归西数(创建新的任务)去执行子文件夹的扫描和保存工作
c. 遍历完毕,即当前线程的for循环走完,这一级目录下的文件夹(创建了新线程递归处理)和文件的保存扫描任务执行结束(子线程个数-1)
d. 当子线程个数变为0时,唤醒主线程
package task;
import callback.FileScannerCallback;
import lombok.Getter;
import java.io.File;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 进行文件扫描任务
*/
@Getter
public class FileScanner {
// 多线程统计个数时,用多线程下的原子类保证线程安全
// 当前扫描的文件个数
private AtomicInteger fileNum = new AtomicInteger();
// 当前扫描的文件夹个数
// 最开始扫描的根路径没有统计,因此初始化文件夹的个数为1,表示从根目录下开始进行扫描任务
private AtomicInteger dirNum = new AtomicInteger(1);
// 所有扫描文件的子线程个数,只有当子线程个数为0时,主线程才继续执行
private AtomicInteger threadCount = new AtomicInteger();
//当最后一个子线程执行完毕后,再调用CountDown方法唤醒主线程
private CountDownLatch latch = new CountDownLatch(1);
// 获取当前电脑的CPU个数,作为核心线程个数
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// 使用线程池创建对象
private ThreadPoolExecutor pool = new ThreadPoolExecutor(CPU_COUNT,CPU_COUNT*2,10, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(), new ThreadPoolExecutor.AbortPolicy());
// 文件扫描回调函数,一个接口对象
private FileScannerCallback callBack;
public FileScanner(FileScannerCallback callBack){
this.callBack = callBack;
}
/**
* 根据传入的文件夹进行扫描任务
* @param filePath
*/
public void scan(File filePath){
System.out.println("开始文件扫描任务,根目录为 : " + filePath);
long start = System.nanoTime();
// 将具体的扫描任务交给子线程处理
threadCount.incrementAndGet();
scanInternal(filePath); // 创建子线程处理根目录的扫描任务
try {
latch.await(); // 主线程进入等待状态
} catch (InterruptedException e) {
System.err.println("扫描任务中断,根目录为 : " + filePath);
}finally {
System.out.println("关闭线程池.....");
// 若所有子线程已经执行结束,就是正常关闭
// 若中断任务,需要立即停止所有还在扫描的子线程
pool.shutdownNow();
}
// 计算扫描时间 以及 扫描到的文件个数
long end = System.nanoTime();
System.out.println("文件扫描任务结束,共耗时 : " + (end - start) * 1.0 / 1000000 + "ms");
System.out.println("文件扫描任务结束,根目录为 : " + filePath);
System.out.println("共扫描到 : " + fileNum.get() + "个文件");
System.out.println("共扫描到 : " + dirNum.get() + "个文件夹");
}
/**
* 具体扫描任务的子线程递归方法
* @param filePath
*/
private void scanInternal(File filePath) {
if (filePath == null) {
return;
}
// 将当前要扫描的任务交给线程处理
pool.submit(()->{
// 使用回调函数,将当前目录下的所有内容保存到指定终端
this.callBack.callback(filePath);
// 先将当前这一级目录下的file对象获取出来
File[] files = filePath.listFiles();
// 遍历这些file对象,根据是否是文件夹进行区别处理
for (File file : files) {
if (file.isDirectory()) {
// 文件夹
dirNum.incrementAndGet(); // 等同于 ++i
// 创建新的子线程,将子文件夹的任务交给新线程处理
threadCount.incrementAndGet(); // 线程总数加1
scanInternal(file);
}else {
// 文件
fileNum.getAndIncrement(); // 等同于 i++
}
}
// 当前线程的保存扫描任务执行结束,子线程数 --
System.out.println(Thread.currentThread().getName() + "扫描 : " + filePath + "任务结束");
threadCount.decrementAndGet();
// 当子线程个数为0时,说明所有子线程的任务都结束了,可以唤醒主线程
if(threadCount.get()==0){
System.out.println("所有扫描任务结束");
// 唤醒主线程
latch.countDown();
}
});
}
}
作用:根据选择的文件夹路径和用户输入的内容从数据库中查找出指定的内容并返回
package task;
import app.FileMeta;
import util.DBUtil;
import util.PinyinUtil;
import java.io.File;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 根据选择的文件夹路径和用户输入的内容从数据库中查找出指定的内容并返回
*/
public class FileSearch {
/**
* 根据条件找到界面要显示的内容,并返回
* @param dir 用户选择的文件夹路径,一定不为空
* @param content 搜索框中的内容。可能为空,若为空就展示数据库中当前选择的路径下的所有内容即可
* @return
*/
public static List search(String dir, String content){
List result = new ArrayList<>();
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
try{
connection = DBUtil.getConnection();
// 先根据用户选择的文件夹dir查询内容
// 查询当前路径下的文件和文件夹 以及 子文件夹里的所有内容
String sql = "select name,path,size,is_directory,last_modified from file_meta"
+ " where ( path = ? or path like ? )";
if(content!=null && content.trim().length()!=0){
// 此时用户搜索框中的内容不为空,此处支持文件全名称,拼音全名称,以及拼音首字母的模糊查询
// 根据搜索框的内容查询数据库,都是模糊匹配
sql += " and ( name like ? or pinyin like ? or pinyin_first like ? )";
}
ps = connection.prepareStatement(sql);
ps.setString(1,dir);
ps.setString(2,dir + File.separator + "%");
if(content!=null && content.trim().length()!=0){
ps.setString(3,"%" + content + "%");
ps.setString(4,"%" + content + "%");
ps.setString(5,"%" + content + "%");
}
rs = ps.executeQuery();
while (rs.next()){
FileMeta meta = new FileMeta();
meta.setName(rs.getString("name"));
meta.setPath(rs.getString("path"));
meta.setIsDirectory(rs.getBoolean("is_directory"));
meta.setLastModified(new Date(rs.getTimestamp("last_modified").getTime()));
if(!meta.getIsDirectory()){
// 文件,保存大小
meta.setSize(rs.getLong("size"));
}
// System.out.println("检索到文件信息 : name = " + meta.getName() + ",path = " + meta.getPath());
result.add(meta);
}
}catch (SQLException e) {
System.err.println("从数据库中搜索用户查找内容时出错,请检查SQL语句");
e.printStackTrace();
}finally {
DBUtil.close(ps,rs);
}
return result;
}
}
作用:文件信息扫描的回调接口,扫描文件时由具体的子类决定将当前目录下的文件信息持久化到哪个终端。可以是数据库,也可以通过网络传输等。
/**
* 文件信息扫描的回调接口
*/
public interface FileScannerCallback {
/**
* 文件扫描的回调接口,扫描文件时由具体的子类决定将当前目录下的文件信息持久化到哪个终端
* @param dir
*/
void callback(File dir);
}
作用:FileScannerCallback接口的实现子类,将当前目下扫描到的文件信息传到SQLite数据库中
功能:
在当前路径下某个文件夹名字被更改后,再次扫描该文件夹时,原先名字文件夹中的所有内容应该被删除,然后将改名后的文件夹下的所有内容添加到数据库中(因为路径变了所以要更改)。但对于其他未更改名字的文件夹没有影响。
实现思路:
选择一个目录后,先将当前目录中的所有文件信息保存到缓存中(内存)——保证数据一定是操作系统中最新的文件信息
从数据库中查询到当前目录下的所有文件信息
对比这两个文件列表
操作系统存在但数据库不存在==>最新的内容,需要插入
操作系统不存在但数据库存在==>过期数据,需要从数据库中删除
package callback.impl;
import app.FileMeta;
import callback.FileScannerCallback;
import util.DBUtil;
import util.PinyinUtil;
import java.io.File;
import java.sql.*;
import java.util.*;
import java.util.Date;
/**
* 传输数据到数据库
*/
public class FileSave2DB implements FileScannerCallback {
@Override
public void callback(File dir){
// 列举出当前dir路径下的所有文件对象
File[] files = dir.listFiles();
// 边界条件: files为空,说明当前路径下没有文件或者dir不是个文件夹(可能由某些类调用传入了一个非文件对象)
if(files != null && files.length != 0){
// 1. 先将当前dir下的所有文件信息保存到内存中,缓存中的信息就是操作系统中的最新数据
List locals = new ArrayList<>();
for (File file : files) {
FileMeta meta = new FileMeta();
if(file.isDirectory()){
// 文件夹
setCommonFiled(file.getName(), file.getParent(), true, file.lastModified(), meta);
}else {
// 文件
setCommonFiled(file.getName(), file.getParent(), false, file.lastModified(), meta);
meta.setSize(file.length());
}
locals.add(meta);
}
// 2. 从数据库中查询出当前路径下的所有文件信息
List dbFiles = query(dir);
// 3. 对比操作系统中最新文件信息 和 数据库中存储的文件信息
// 本地有,数据库中没有的,做插入
// 遍历locals(数据库结果集可能为空),若dbFiles中不存在,插入
for(FileMeta meta : locals){
if(!dbFiles.contains(meta)){
save(meta);
}
}
// 数据库有,本地没有的,做删除
// 遍历数据库,locals中不存在的,做删除
for (FileMeta meta : dbFiles) {
if(!locals.contains(meta)){
delete(meta);
}
}
}
}
/**
* 从数据库中删除指定记录
* @param meta
*/
private void delete(FileMeta meta) {
Connection connection = null;
PreparedStatement ps = null;
try{
connection = DBUtil.getConnection();
// 删除文件本身
String sql = "delete from file_meta where"+
" (name = ? and path = ?)";
// 如果当前对象是个文件夹
if(meta.getIsDirectory()){
// 删除文件夹内部的子文件和子文件夹
sql += " or path = ?"; // 删除第一级子文件
sql += " or path like ?"; // 删除多级子文件
}
ps = connection.prepareStatement(sql);
ps.setString(1, meta.getName());
ps.setString(2, meta.getPath());
if(meta.getIsDirectory()){
ps.setString(3, meta.getPath()+ File.separator +meta.getName());
ps.setString(4,meta.getPath()+ File.separator +meta.getName()
+ File.separator + "%");
}
// System.out.println("执行删除操作,sql为:" + ps);
int rows = ps.executeUpdate();
// if(meta.getIsDirectory()){
// System.out.println("删除文件夹"+meta.getName()+"成功,共删除"+rows+"个文件");
// }else{
// System.out.println("删除文件"+meta.getName()+"成功");
// }
}catch (SQLException e){
System.err.println("文件删除出错,请检查SQL语句");
e.printStackTrace();
}finally {
DBUtil.close(ps);
}
}
/**
* 保存指定文件对象到数据库
* @param meta
*/
private void save(FileMeta meta) {
Connection connection = null;
PreparedStatement ps = null;
try{
connection = DBUtil.getConnection();
String sql = "insert into file_meta values(?,?,?,?,?,?,?)";
ps = connection.prepareStatement(sql);
String fileName = meta.getName();
ps.setString(1,fileName);
ps.setString(2, meta.getPath());
ps.setBoolean(3, meta.getIsDirectory());
if(!meta.getIsDirectory()){
// 当前文件对象是文件时设置文件长度
ps.setLong(4,meta.getSize());
}
ps.setTimestamp(5,new Timestamp(meta.getLastModified().getTime()));
// 需要判断文件名是否包含中文
if(PinyinUtil.containsChinese(fileName)){
String[] pinyins = PinyinUtil.getPinyinByFileName(fileName);
ps.setString(6,pinyins[0]); // 全拼
ps.setString(7,pinyins[1]); // 首字母
}
// System.out.println("执行文件的保存操作,SQL为:"+ps);
int rows = ps.executeUpdate();
// System.out.println("成功保存"+ rows +"行文件信息");
}catch (SQLException e){
System.err.println("保存文件信息出错,请检查SQL语句");
e.printStackTrace();
}finally {
DBUtil.close(ps);
}
}
/**
* 查询数据库中指定路径下的文件信息
* @param dir
* @return
*/
private List query(File dir) {
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
List dbFiles = new ArrayList<>();
try{
connection = DBUtil.getConnection();
String sql = "select name,path,is_directory,size,last_modified from file_meta" +
// 切记sql拼接时,换行需要加空格
" where path = ?";
ps = connection.prepareStatement(sql);
ps.setString(1,dir.getPath());
rs = ps.executeQuery();
// System.out.println("查询指定路径的SQL为 : " + ps);
while (rs.next()){
FileMeta meta = new FileMeta();
meta.setName(rs.getString("name"));
meta.setPath(rs.getString("path"));
meta.setIsDirectory(rs.getBoolean("is_directory"));
meta.setLastModified(new Date(rs.getTimestamp("last_modified").getTime()));
// 只有在当前路径是文件时才设置size大小,若不是文件夹,不设置size大小
if(!meta.getIsDirectory()){
meta.setSize(rs.getLong("size"));
}
dbFiles.add(meta);
System.out.println("查询到的文件名为:"+ meta.getName());
}
}catch (SQLException e){
System.err.println("查询数据库指定路径下的文件出错,请检查SQL语句");
e.printStackTrace();
}finally {
DBUtil.close(ps,rs);
}
return dbFiles;
}
/**
* 设置扫描到的目录信息,减少重复设置代码
* @param name
* @param path
* @param isDirectory
* @param lastModified
* @param meta
*/
private void setCommonFiled(String name, String path, boolean isDirectory, Long lastModified, FileMeta meta) {
meta.setName(name);
meta.setPath(path);
meta.setIsDirectory(isDirectory);
// 由于file对象的lastModified是一个长整型,以时间戳为单位
// 因此需要转化成和FileMetad对象中lastModified一致的Date类型
meta.setLastModified(new Date(lastModified));
}
}
具体测试过程见博客:项目测试:Search_Everything_啊夏同学的博客-CSDN博客
初始界面
文件选择
文件扫描结果展示
文件名搜索
拼音搜索
拼音首字母搜索
后缀名搜索