接前一篇 VFS - 代码生成器预览功能实现 ,上一篇讲到了 mkdirs
封装创建目录的方法,接下来先处理前文中的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中的文件名和类型的规则和操作系统就一致了,为后面和操作系统上的文件系统交互打下了基础。
在 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; }
下面开始基于这个基础方法开始实现文件的基本操作,先看删除文件。
和 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; }
删除非常简单,下面的写入文件也很简单。
为了支持更多类型的文件,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,下一篇文章在详细介绍。
由于代码还在变,恐怕最后一篇文章写出前不适合再发源码,等最后再重新给所有在博客、微信留邮箱的各位读者朋友发送一遍代码。