接前一篇 VFS - 代码生成器预览功能实现 ,上一篇讲到了 mkdirs
封装创建目录的方法,接下来先处理前文中的BUG,然后再封装文件的基础方法。
文件的 BUG
在前一篇文章中,认为一个文件的 name
和 type
同时决定了唯一的一个文件,这个设计没有问题,但是经过在不同操作系统测试发现,同一个文件名只能在一个目录中出现一次,名字决定了唯一的一个文件,类型决定了可以对这个文件进行什么样的操作。并且默认情况下只有 Linux 对文件名区分大小写,Window 和 macOS 默认都不区分大小写,因为文件名的类型为 Path
,我特地看了看不同操作系统下 Path 的比较方式,JDK 两类系统的源码实现:
Windows 的实现中最终比较 Path 时会转换为大写进行比较,Unix 实现不会进行转换。因为文件名使用的 Path
类型,所以直接比较 Path name
就支持了不同操作系统的不同实现。
由于 macOS 本身默认不区分大小写(和磁盘分区格式有关),但是 macOS 的 jdk 实现使用的
UnixPath
,这就产生了一个 Java 的BUG:macOS 的代码上区分大小写,真正创建文件时又不区分大小写,会导致代码上多出的文件丢失。例如先创建一个
a.txt
文件,在创建一个A.txt
文件时,代码中认为有两个文件,实际硬盘上只有一个a.txt
文件,当两个文件依次写入内容时,第二个文件的内容会覆盖a.txt
的内容。所以在真正写代码时,文件名不能区分大小写。
先修改下面两个基础方法:
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
VFSNode vfsNode = (VFSNode) o;
return name.equals(vfsNode.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
由于只通过文件名判断文件是否已经存在,因此添加直接的子文件时增加判断已经存在的文件类型是否一致:
private void addChild(VFSNode child) {
if (CollUtil.isEmpty(this.files)) {
this.files = new ArrayList<>();
}
if (this.files.contains(child)) {
VFSNode same = getChild(child.name);
if (same.type != child.type) {
throw new RuntimeException("已经存在类型为 "
+ same.type + " 的文件,无法添加 " + child.type + " 类型");
}
} else {
this.files.add(child);
child.parent = this;
}
}
最后一个改动的地方就是添加子文件过程中,需要判断中间的文件是否为目录,如果不是目录也不允许往下添加子文件。
protected void addVFSNode(VFSNode node, Path relativePath) {
int nameCount = relativePath.getNameCount();
if (nameCount > 1) {
Path name = relativePath.getName(0);
VFSNode vfsNode = getChild(name);
if (vfsNode == null) {
vfsNode = new VFSNode(name, Type.DIR);
addChild(vfsNode);
}
//增加判断节点类型
if(vfsNode.isDirectory()) {
vfsNode.addVFSNode(node, relativePath.subpath(1, nameCount));
} else {
throw new RuntimeException("无法向文件 " + vfsNode.name + " 下添加子文件");
}
} else if (nameCount == 1) {
addChild(node);
}
}
经过上面的修改后,VFS中的文件名和类型的规则和操作系统就一致了,为后面和操作系统上的文件系统交互打下了基础。
VFS 文件基本操作
在 VFSNode
中通过简单的 write
和 read
方法实现了虚拟文件的读写,而且文件还支持多次覆盖写入,并且记录文件写入内容的历史,在外层 VFS 中封装时,仍然是先要通过相对路径找到要写入的文件,如果文件不存在还要先创建该文件。找到要写入的虚拟文件后,调用 VFSNode
的写入方法就可以实现。在 VFSNode
中也提过 找到要操作的文件是其他操作的基础,这里还是先封装该方法:
/**
* 查找指定类型节点
*
* @param relativePath 相对路径
* @return
*/
protected VFSNode findVFSNode(Path relativePath) {
return findVFSNode(relativePath, null, false);
}
/**
* 查找指定类型节点
*
* @param relativePath 相对路径
* @param type 文件类型
* @return
*/
protected VFSNode findVFSNode(Path relativePath, Type type) {
return findVFSNode(relativePath, type, false);
}
/**
* 查找节点
*
* @param relativePath 相对路径
* @param type 文件类型
* @param createIfNotExists 如果不存在就创建,这种情况要么返回节点要么抛出异常
* @return
*/
protected VFSNode findVFSNode(Path relativePath, Type type, boolean createIfNotExists) {
//检查相对路径是否合法(不能超出根路径范围)
checkRelativePath(relativePath);
//根据相对路径查找节点
VFSNode node = getVFSNode(relativePath);
//当节点存在、指定了类型、并且类型不一致时
if (node != null && type != null && node.type != type) {
//如果需要创建就抛出异常
if (createIfNotExists) {
throw new RuntimeException("已经存在类型为 " + node.type
+ " 的文件,无法创建 " + type + "类型的同名文件");
}
//不需要创建就因为类型不一致返回null
return null;
}
//文件不存在并且需要创建时
if (node == null && createIfNotExists) {
//新建节点
node = new VFSNode(relativePath.getFileName(), type);
//根据相对路径添加节点
addVFSNode(node, relativePath);
}
return node;
}
下面开始基于这个基础方法开始实现文件的基本操作,先看删除文件。
VFS 文件操作 - 删除
和 mkdirs
方法类似,有三种形式参数的方法,删除文件时,查找到对应的 VFSNode
调用对象上的 delete
方法断绝和父级的关系即可。
/**
* 删除指定文件
*
* @param file 文件
* @return true 删除成功,false 文件不存在
*/
public boolean delelte(File file) {
return delelte(relativize(file));
}
/**
* 删除指定文件
*
* @param relativePath 相对路径
* @return true 删除成功,false 文件不存在
*/
public boolean delelte(String relativePath) {
return delelte(toPath(relativePath));
}
/**
* 删除相对路径的文件
*
* @param relativePath 相对路径
* @return true 删除成功,false 文件不存在
*/
public boolean delelte(Path relativePath) {
VFSNode vfsNode = findVFSNode(relativePath);
if (vfsNode != null) {
vfsNode.delete();
return true;
}
return false;
}
删除非常简单,下面的写入文件也很简单。
VFS 文件操作 - 写文件
为了支持更多类型的文件,VFSNode
中的文件内容使用的 byte[]
字节数组类型,因此首先提供写入 bytes[]
的方法:
/**
* 写入文件内容
*
* @param file 文件
* @param bytes 内容
*/
public void write(File file, byte[] bytes) {
write(relativize(file), bytes);
}
/**
* 写入文件内容
*
* @param relativePath 相对路径
* @param bytes 内容
*/
public void write(String relativePath, byte[] bytes) {
write(toPath(relativePath), this.bytes);
}
/**
* 写入相对文件内容
*
* @param relativePath 相对路径
* @param bytes 文件内容
*/
public void write(Path relativePath, byte[] bytes) {
//获取文件并写入数据,获取时指定为 FILE 类型,如果文件不存在就创建
findVFSNode(relativePath, Type.FILE, true).write(bytes);
}
byte[]
类型的内容更通用,String
类型的文件比 byte[]
更常用。
/**
* 写入文件内容
*
* @param file 文件
* @param content 内容
*/
public void write(File file, String content) {
write(relativize(file), content);
}
/**
* 写入文件内容
*
* @param relativePath 相对路径
* @param content 内容
*/
public void write(String relativePath, String content) {
write(toPath(relativePath), content);
}
/**
* 写入相对文件内容
*
* @param relativePath 相对路径
* @param content 文件内容
*/
public void write(Path relativePath, String content) {
findVFSNode(relativePath, Type.FILE, true).write(content.getBytes(StandardCharsets.UTF_8));
}
基本操作功能测试
VFS vfs = VFS.of("/");
vfs.mkdirs("/a");
vfs.mkdirs("/a/b");
vfs.mkdirs("/a/c");
vfs.mkdirs("/a/c");
vfs.mkdirs("/a/d/e.txt");
vfs.write("/a/help.txt", "帮助文档");
vfs.delelte("/a/c");
System.out.println(vfs.print());
输出的文件结构如下:
/
└── a
├── b
├── d
│ └── e.txt
└── help.txt
又是未完,待续…
又写了很长时间还没写完,一个是想表达的内容比较多,写的太长怕一次读不完,另外更主要的原因是:写文章的时候要换一个角度对代码进行重新理解并展示给读者,重新理解的过程也涉及到了代码的重构,因此写文章的同时也在写代码。
虽然未完,但是后续要写的整体内容基本上已经明确了,后续还有一篇,主要是加载指定的目录到VFS中,将VFS的内容写入到指定的目录中。除了目录外,为了便于使用专门增加了 ZIP 文件的支持,这次 ZIP 导入导出的功能让我又重新认识了 Java 的 ZIP,下一篇文章在详细介绍。
由于代码还在变,恐怕最后一篇文章写出前不适合再发源码,等最后再重新给所有在博客、微信留邮箱的各位读者朋友发送一遍代码。