From ec1c7f63affe3d883904a2f3ab9768739b1f9058 Mon Sep 17 00:00:00 2001 From: Markus Ebner Date: Tue, 8 Nov 2022 01:15:47 +0100 Subject: [PATCH 01/10] Implement SchematicsManager to cache list of known schematics --- .../java/com/sk89q/worldedit/WorldEdit.java | 11 + .../worldedit/command/SchematicCommands.java | 40 ++-- .../extension/platform/Capability.java | 3 + .../io/file/RecursiveDirectoryWatcher.java | 226 ++++++++++++++++++ .../worldedit/util/schematic/Schematic.java | 53 ++++ .../util/schematic/SchematicsManager.java | 118 +++++++++ .../backends/DummySchematicsBackend.java | 49 ++++ .../FileWatcherSchematicsBackend.java | 98 ++++++++ .../backends/PollingSchematicsBackend.java | 110 +++++++++ .../schematic/backends/SchematicsBackend.java | 52 ++++ 10 files changed, 734 insertions(+), 26 deletions(-) create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/RecursiveDirectoryWatcher.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/Schematic.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java b/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java index 246c736c84..abe09f29b6 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java @@ -64,6 +64,7 @@ import com.sk89q.worldedit.util.io.file.FilenameException; import com.sk89q.worldedit.util.io.file.FilenameResolutionException; import com.sk89q.worldedit.util.io.file.InvalidFilenameException; +import com.sk89q.worldedit.util.schematic.SchematicsManager; import com.sk89q.worldedit.util.task.SimpleSupervisor; import com.sk89q.worldedit.util.task.Supervisor; import com.sk89q.worldedit.util.translation.TranslationManager; @@ -127,6 +128,7 @@ public final class WorldEdit { EvenMoreExecutors.newBoundedCachedThreadPool(0, 1, 20, "WorldEdit Task Executor - %s")); private final Supervisor supervisor = new SimpleSupervisor(); private final AssetLoaders assetLoaders = new AssetLoaders(this); + private final SchematicsManager schematicsManager = new SchematicsManager(this); private final BlockFactory blockFactory = new BlockFactory(this); private final ItemFactory itemFactory = new ItemFactory(this); @@ -262,6 +264,15 @@ public AssetLoaders getAssetLoaders() { return assetLoaders; } + /** + * Return the Schematics Manager instance. + * + * @return the schematics manager instance + */ + public SchematicsManager getSchematicsManager() { + return schematicsManager; + } + /** * Gets the path to a file. This method will check to see if the filename * has valid characters and has an extension. It also prevents directory diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java index eba358dd33..8084a41fe7 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java @@ -55,6 +55,8 @@ import com.sk89q.worldedit.util.io.Closer; import com.sk89q.worldedit.util.io.file.FilenameException; import com.sk89q.worldedit.util.io.file.MorePaths; +import com.sk89q.worldedit.util.schematic.Schematic; +import com.sk89q.worldedit.util.schematic.SchematicsManager; import org.apache.logging.log4j.Logger; import org.enginehub.piston.annotation.Command; import org.enginehub.piston.annotation.CommandContainer; @@ -314,7 +316,6 @@ public void list(Actor actor, if (oldFirst && newFirst) { throw new StopExecutionException(TextComponent.of("Cannot sort by oldest and newest.")); } - final String saveDir = worldEdit.getConfiguration().saveDir; Comparator pathComparator; String flag; if (oldFirst) { @@ -330,8 +331,10 @@ public void list(Actor actor, final String pageCommand = actor.isPlayer() ? "//schem list -p %page%" + flag : null; + Comparator schematicComparator = (s0, s1) -> pathComparator.compare(s0.getPath(), s1.getPath()); + WorldEditAsyncCommandBuilder.createAndSendMessage(actor, - new SchematicListTask(saveDir, pathComparator, page, pageCommand), + new SchematicListTask(schematicComparator, page, pageCommand), SubtleFormat.wrap("(Please wait... gathering schematic list.)")); } @@ -399,6 +402,7 @@ private static class SchematicSaveTask extends SchematicOutputTask { public Void call() throws Exception { try { writeToOutputStream(new FileOutputStream(file)); + WorldEdit.getInstance().getSchematicsManager().update(); LOGGER.info(actor.getName() + " saved " + file.getCanonicalPath() + (overwrite ? " (overwriting previous file)" : "")); } catch (IOException e) { file.delete(); @@ -437,22 +441,20 @@ public Consumer call() throws Exception { } private static class SchematicListTask implements Callable { - private final Comparator pathComparator; + private final Comparator pathComparator; private final int page; - private final Path rootDir; private final String pageCommand; - SchematicListTask(String prefix, Comparator pathComparator, int page, String pageCommand) { + SchematicListTask(Comparator pathComparator, int page, String pageCommand) { this.pathComparator = pathComparator; this.page = page; - this.rootDir = WorldEdit.getInstance().getWorkingDirectoryPath(prefix); this.pageCommand = pageCommand; } @Override public Component call() throws Exception { - Path resolvedRoot = rootDir.toRealPath(); - List fileList = allFiles(resolvedRoot); + SchematicsManager schematicsManager = WorldEdit.getInstance().getSchematicsManager(); + List fileList = schematicsManager.getList(); if (fileList.isEmpty()) { return ErrorFormat.wrap("No schematics found."); @@ -460,30 +462,16 @@ public Component call() throws Exception { fileList.sort(pathComparator); - PaginationBox paginationBox = new SchematicPaginationBox(resolvedRoot, fileList, pageCommand); + PaginationBox paginationBox = new SchematicPaginationBox(schematicsManager.getRoot(), fileList, pageCommand); return paginationBox.create(page); } } - private static List allFiles(Path root) throws IOException { - List pathList = new ArrayList<>(); - try (DirectoryStream stream = Files.newDirectoryStream(root)) { - for (Path path : stream) { - if (Files.isDirectory(path)) { - pathList.addAll(allFiles(path)); - } else { - pathList.add(path); - } - } - } - return pathList; - } - private static class SchematicPaginationBox extends PaginationBox { private final Path rootDir; - private final List files; + private final List files; - SchematicPaginationBox(Path rootDir, List files, String pageCommand) { + SchematicPaginationBox(Path rootDir, List files, String pageCommand) { super("Available schematics", pageCommand); this.rootDir = rootDir; this.files = files; @@ -492,7 +480,7 @@ private static class SchematicPaginationBox extends PaginationBox { @Override public Component getComponent(int number) { checkArgument(number < files.size() && number >= 0); - Path file = files.get(number); + Path file = files.get(number).getPath(); String format = ClipboardFormats.getFileExtensionMap() .get(MoreFiles.getFileExtension(file)) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Capability.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Capability.java index be34b5e183..7be3a8b88d 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Capability.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Capability.java @@ -21,6 +21,7 @@ import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.internal.block.BlockStateIdAccess; +import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.registry.BlockRegistry; @@ -53,10 +54,12 @@ void uninitialize(PlatformManager platformManager, Platform platform) { @Override void initialize(PlatformManager platformManager, Platform platform) { WorldEdit.getInstance().getAssetLoaders().init(); + WorldEdit.getInstance().getSchematicsManager().init(); } @Override void uninitialize(PlatformManager platformManager, Platform platform) { + WorldEdit.getInstance().getSchematicsManager().uninit(); WorldEdit.getInstance().getAssetLoaders().uninit(); } }, diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/RecursiveDirectoryWatcher.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/RecursiveDirectoryWatcher.java new file mode 100644 index 0000000000..849b816967 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/RecursiveDirectoryWatcher.java @@ -0,0 +1,226 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.io.file; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.sk89q.worldedit.internal.util.LogManagerCompat; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.nio.file.ClosedWatchServiceException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.function.Consumer; + +/** + * Helper class that allows to recursively monitor a directory for changed (created / deleted) files / folders. + * + * @warning File- and Folder events might be sent multiple times. Users of this class need to employ their own + * deduplication! + */ +public class RecursiveDirectoryWatcher { + + /** + * Base-Class for all DirEntry change events. + */ + public static class DirEntryChangeEvent { + private Path path; + + public DirEntryChangeEvent(Path path) { + this.path = path; + } + + public Path getPath() { + return path; + } + } + + /** + * Event signaling the creation of a new file. + */ + public static class FileCreatedEvent extends DirEntryChangeEvent { + public FileCreatedEvent(Path path) { + super(path); + } + } + + /** + * Event signaling the deletion of a file. + */ + public static class FileDeletedEvent extends DirEntryChangeEvent { + public FileDeletedEvent(Path path) { + super(path); + } + } + + /** + * Event signaling the creation of a new directory. + */ + public static class DirectoryCreatedEvent extends DirEntryChangeEvent { + public DirectoryCreatedEvent(Path path) { + super(path); + } + } + + /** + * Event signaling the deletion of a directory. + */ + public static class DirectoryDeletedEvent extends DirEntryChangeEvent { + public DirectoryDeletedEvent(Path path) { + super(path); + } + } + + + private static final Logger LOGGER = LogManagerCompat.getLogger(); + + private final Path root; + private final WatchService watchService; + private Thread watchThread; + private Consumer eventConsumer; + private BiMap watchRootMap = HashBiMap.create(); + + private RecursiveDirectoryWatcher(Path root, WatchService watchService) { + this.root = root; + this.watchService = watchService; + } + + /** + * Create a new recursive directory watcher for the given root folder. + * You have to call @see start() before the instance starts monitoring. + * + * @param root Folder to watch for changed files recursively. + * @return A new RecursiveDirectoryWatcher instance, monitoring the given root folder. + * @throws IOException If creating the watcher failed, e.g. due to root not existing. + */ + public static RecursiveDirectoryWatcher create(Path root) throws IOException { + WatchService watchService = root.getFileSystem().newWatchService(); + return new RecursiveDirectoryWatcher(root, watchService); + } + + private void registerFolderWatchAndScanInitially(Path root) throws IOException { + WatchKey watchKey = root.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE); + LOGGER.debug("Watch registered: " + root); + watchRootMap.put(watchKey, root); + eventConsumer.accept(new DirectoryCreatedEvent(root)); + for (Path path : Files.newDirectoryStream(root)) { + if (Files.isDirectory(path)) { + registerFolderWatchAndScanInitially(path); + } else { + eventConsumer.accept(new FileCreatedEvent(path)); + } + } + } + + /** + * Make this RecursiveDirectoryWatcher instance start monitoring the root folder it was created on. + * When this is called, RecursiveDirectoryWatcher will send initial notifications for the entire + * file structure in the configured root. + * @param eventConsumer The lambda that's fired for every file event. + */ + public void start(Consumer eventConsumer) { + this.eventConsumer = eventConsumer; + watchThread = new Thread(() -> { + LOGGER.debug("RecursiveDirectoryWatcher::EventConsumer started"); + + try { + registerFolderWatchAndScanInitially(root); + } catch (IOException e) { e.printStackTrace(); } + + try { + WatchKey watchKey; + while (true) { + try { + watchKey = watchService.take(); + } catch (InterruptedException e) { break; } + + for (WatchEvent event : watchKey.pollEvents()) { + WatchEvent.Kind kind = event.kind(); + if (kind.equals(StandardWatchEventKinds.OVERFLOW)) { + LOGGER.warn("RecursiveDirectoryWatcher Seems like we can't keep up with updates"); + continue; + } + // make sure to work with an absolute path + Path path = (Path) event.context(); + Path parentPath = watchRootMap.get(watchKey); + path = parentPath.resolve(path); + + if (kind.equals(StandardWatchEventKinds.ENTRY_CREATE)) { + if (Files.isDirectory(path)) { // new subfolder created, create watch for it + try { + registerFolderWatchAndScanInitially(path); + } catch (IOException e) { e.printStackTrace(); } + } else { // new file created + eventConsumer.accept(new FileCreatedEvent(path)); + } + } else if (kind.equals(StandardWatchEventKinds.ENTRY_DELETE)) { + // When we are notified about a deleted entry, we can't simply ask the filesystem + // whether the entry is a file or a folder. But we have our watchRootMap, that stores + // one WatchKey per (sub)folder, so we can just ask it. + if (watchRootMap.containsValue(path)) { // was a folder + LOGGER.debug("Watch unregistered: " + path); + WatchKey obsoleteSubfolderWatchKey = watchRootMap.inverse().get(path); + // stop listening to changes from deleted dir + obsoleteSubfolderWatchKey.cancel(); + watchRootMap.remove(obsoleteSubfolderWatchKey); + eventConsumer.accept(new DirectoryDeletedEvent(path)); + } else { // was a file + eventConsumer.accept(new FileDeletedEvent(path)); + } + } + } + + if (!watchKey.reset()) { + watchRootMap.remove(watchKey); + if (watchRootMap.isEmpty()) { + break; // nothing left to watch + } + } + } + } catch (ClosedWatchServiceException ignored) { } + LOGGER.debug("RecursiveDirectoryWatcher::EventConsumer exited"); + }); + watchThread.setName("RecursiveDirectoryWatcher"); + watchThread.start(); + } + + /** + * Stop this RecursiveDirectoryWatcher instance and wait for it to be completely shut down. + * @warning RecursiveDirectoryWatcher is not reusable! + */ + public void stop() { + try { + watchService.close(); + } catch (IOException e) { e.printStackTrace(); } + if (watchThread != null) { + try { + watchThread.join(); + } catch (InterruptedException e) { e.printStackTrace(); } + watchThread = null; + } + eventConsumer = null; + } + +} \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/Schematic.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/Schematic.java new file mode 100644 index 0000000000..69317c2eb8 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/Schematic.java @@ -0,0 +1,53 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.schematic; + +import java.nio.file.Path; + +/** + * Record representing one Schematic file. + */ +public record Schematic(Path path) { + + /** + * Get this Schematic's path. + * @return This Schematic's path. + */ + public Path getPath() { + return path; + } + + @Override + public String toString() { + return path.toString(); + } + + @Override + public int hashCode() { + return path.hashCode(); + } + + @Override + public boolean equals(Object obj) { + Schematic other = (Schematic) obj; + if (other == null) { return false; } + return path.equals(other.getPath()); + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java new file mode 100644 index 0000000000..c1adc4cd8e --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java @@ -0,0 +1,118 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.schematic; + +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.internal.util.LogManagerCompat; +import com.sk89q.worldedit.util.schematic.backends.DummySchematicsBackend; +import com.sk89q.worldedit.util.schematic.backends.FileWatcherSchematicsBackend; +import com.sk89q.worldedit.util.schematic.backends.PollingSchematicsBackend; +import com.sk89q.worldedit.util.schematic.backends.SchematicsBackend; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * Class that manages the known Schematic files. + * + *

This class monitors the schematics folder for changes and keeps an always-up-to-date list of known + * schematics in RAM in order to speed up queries. + * This further also allows more convenient features like supporting command suggestions for known schematics. + * + *

If initialization of the inotify Backend fails, SchematicsManager is going to fall back to a polled variant, where + * the result is cached for a certain amount of time, before a rescan is performed. (Eventual Consistency Cache) + */ +public class SchematicsManager { + + private static final Logger LOGGER = LogManagerCompat.getLogger(); + private WorldEdit worldEdit; + + private Path schematicsDir; + private SchematicsBackend backend; + + public SchematicsManager(WorldEdit worldEdit) { + this.worldEdit = worldEdit; + } + + private void setupBackend() { + try { + backend = FileWatcherSchematicsBackend.create(schematicsDir); + } catch (IOException e) { + LOGGER.warn("Failed to initialize folder-monitoring based Schematics backend. Falling back to scanning."); + backend = PollingSchematicsBackend.create(schematicsDir); + } + } + + /** + * Initialize this SchematicsManager. + * This sets everything up, and initially scans the schematics folder. + */ + public void init() { + try { + schematicsDir = worldEdit.getWorkingDirectoryPath(worldEdit.getConfiguration().saveDir); + Files.createDirectories(schematicsDir); + schematicsDir = schematicsDir.toRealPath(); + setupBackend(); + } catch (IOException e) { + LOGGER.warn("Failed to create schematics directory", e); + backend = new DummySchematicsBackend(); //fallback to dummy backend + } + backend.init(); + } + + /** + * Uninitialize this SchematicsManager. + */ + public void uninit() { + if (backend != null) { + backend.uninit(); + backend = null; + } + } + + /** + * Get the RootPath for schematics. + * @return The root folder where schematics are stored. + */ + public Path getRoot() { + return schematicsDir; + } + + /** + * Get a list of all known schematics. + * @return List of all known schematics. + */ + public List getList() { + return backend.getList(); + } + + /** + * Tell SchematicManager that an update is in order. + * This should be used whenever WorldEdit code adds a new Schematic, to make sure the next list + * response is up-to-date for better user-experience. + */ + public void update() { + backend.update(); + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java new file mode 100644 index 0000000000..7594b1026c --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java @@ -0,0 +1,49 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.schematic.backends; + +import com.sk89q.worldedit.util.schematic.Schematic; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * DummyBackend for the SchematicsManager. + * This is only used in the error-case. For example when creating the schematics folder failed for whatever reason. + */ +public class DummySchematicsBackend implements SchematicsBackend { + @Override + public void init() { + } + + @Override + public void uninit() { + } + + @Override + public List getList() { + return new ArrayList<>(); + } + + @Override + public void update() { + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java new file mode 100644 index 0000000000..038edb09e0 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java @@ -0,0 +1,98 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.schematic.backends; + +import com.sk89q.worldedit.internal.util.LogManagerCompat; +import com.sk89q.worldedit.util.io.file.RecursiveDirectoryWatcher; +import com.sk89q.worldedit.util.schematic.Schematic; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * SchematicsBackend making use of the RecursiveDirectoryWatcher. + * This backend initially scans all schematics in the folder tree and then registers for file change events to + * avoid manually polling / rescanning the folder structure for every query. + */ +public class FileWatcherSchematicsBackend implements SchematicsBackend { + private static final Logger LOGGER = LogManagerCompat.getLogger(); + + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private final Set schematics = new HashSet<>(); + private final RecursiveDirectoryWatcher directoryWatcher; + + private FileWatcherSchematicsBackend(RecursiveDirectoryWatcher directoryWatcher) { + this.directoryWatcher = directoryWatcher; + } + + /** + * Create a new instance of the directory-monitoring SchematicsManager backend. + * @param schematicsFolder Root folder for schematics. + * @return A new FileWatcherSchematicsBackend instance. + * @throws IOException When creation of the filesystem watcher fails. + */ + public static FileWatcherSchematicsBackend create(Path schematicsFolder) throws IOException { + RecursiveDirectoryWatcher watcher = RecursiveDirectoryWatcher.create(schematicsFolder); + return new FileWatcherSchematicsBackend(watcher); + } + + @Override + public void init() { + directoryWatcher.start(event -> { + lock.writeLock().lock(); + if (event instanceof RecursiveDirectoryWatcher.FileCreatedEvent) { + schematics.add(new Schematic(event.getPath())); + } else if (event instanceof RecursiveDirectoryWatcher.FileDeletedEvent) { + schematics.remove(new Schematic(event.getPath())); + } + lock.writeLock().unlock(); + if (event instanceof RecursiveDirectoryWatcher.FileCreatedEvent) { + LOGGER.info("New Schematic found: " + event.getPath()); + } else if (event instanceof RecursiveDirectoryWatcher.FileDeletedEvent) { + LOGGER.info("Schematic deleted: " + event.getPath()); + } + }); + } + + @Override + public void uninit() { + LOGGER.debug("uninit"); + directoryWatcher.stop(); + } + + @Override + public List getList() { + lock.readLock().lock(); + List result = new ArrayList<>(schematics); + lock.readLock().unlock(); + return result; + } + + @Override + public void update() { + // Nothing to do here, we probably already know :) + } +} \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java new file mode 100644 index 0000000000..e47863c433 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java @@ -0,0 +1,110 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.schematic.backends; + +import com.sk89q.worldedit.internal.util.LogManagerCompat; +import com.sk89q.worldedit.util.schematic.Schematic; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * SchematicsBackend implementation that scans the folder tree, then caches the result for a certain amount of time. + * This essentially is an eventually consistent cache that is used as fallback. + */ +public class PollingSchematicsBackend implements SchematicsBackend { + + private static final Logger LOGGER = LogManagerCompat.getLogger(); + + private static final Duration MAX_RESULT_AGE = Duration.ofSeconds(10); + + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private final Path schematicsDir; + private Instant lastUpdateTs = Instant.EPOCH; + private List schematics = new ArrayList<>(); + + private PollingSchematicsBackend(Path schematicsDir) { + this.schematicsDir = schematicsDir; + } + + /** + * Create a new instance of the polling SchematicsManager backend. + * @param schematicsFolder Root folder for schematics. + * @return A new PollingSchematicsBackend instance. + */ + public static PollingSchematicsBackend create(Path schematicsFolder) { + return new PollingSchematicsBackend(schematicsFolder); + } + + private List scanFolder(Path root) { + List pathList = new ArrayList<>(); + try (DirectoryStream stream = Files.newDirectoryStream(root)) { + for (Path path : stream) { + if (Files.isDirectory(path)) { + pathList.addAll(scanFolder(path)); + } else { + pathList.add(new Schematic(path)); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return pathList; + } + + private void runRescan() { + LOGGER.debug("Rescanning Schematics"); + this.schematics = scanFolder(schematicsDir); + lastUpdateTs = Instant.now(); + } + + @Override + public void init() { + LOGGER.debug("init"); + } + + @Override + public void uninit() { + LOGGER.debug("uninit"); + } + + @Override + public synchronized List getList() { + // udpate internal cache if requried (determined by age) + Duration age = Duration.between(lastUpdateTs, Instant.now()); + if (age.compareTo(MAX_RESULT_AGE) >= 0) { + runRescan(); + } + return new ArrayList<>(schematics); + } + + @Override + public synchronized void update() { + lastUpdateTs = Instant.EPOCH; // invalidate cache + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java new file mode 100644 index 0000000000..46c1f111b2 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java @@ -0,0 +1,52 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.schematic.backends; + +import com.sk89q.worldedit.util.schematic.Schematic; + +import java.util.List; + +/** + * SchematicsManager backend interface. + */ +public interface SchematicsBackend { + + /** + * Initialize the SchematicsManager backend. + */ + void init(); + + /** + * Uninitialize the SchematicsManager backend. + */ + void uninit(); + + /** + * Get the list of known schematics. + * + * @return List of known schematics. + */ + List getList(); + + /** + * Tells the backend that there are changes it should take into account. + */ + void update(); +} \ No newline at end of file From e5f9212eacf7e84a6720c9d85f4d14fb21dc32a0 Mon Sep 17 00:00:00 2001 From: Markus Ebner Date: Tue, 8 Nov 2022 01:15:51 +0100 Subject: [PATCH 02/10] Add command suggestions for schematic filenames --- .../worldedit/command/SchematicCommands.java | 12 ++- .../command/argument/SchematicConverter.java | 89 +++++++++++++++++++ .../platform/PlatformCommandManager.java | 2 + 3 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java index 8084a41fe7..566bee2bcf 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java @@ -111,13 +111,15 @@ public SchematicCommands(WorldEdit worldEdit) { @CommandPermissions({"worldedit.clipboard.load", "worldedit.schematic.load"}) public void load(Actor actor, LocalSession session, @Arg(desc = "File name.") - String filename, + Schematic schematic, @Arg(desc = "Format name.", def = "sponge") ClipboardFormat format) throws FilenameException { LocalConfiguration config = worldEdit.getConfiguration(); - File dir = worldEdit.getWorkingDirectoryPath(config.saveDir).toFile(); - File f = worldEdit.getSafeOpenFile(actor, dir, filename, + // Schematic.path is relative, so treat it as filename + String filename = schematic.getPath().toString(); + File schematicsRoot = worldEdit.getSchematicsManager().getRoot().toFile(); + File f = worldEdit.getSafeOpenFile(actor, schematicsRoot, filename, BuiltInClipboardFormat.SPONGE_V3_SCHEMATIC.getPrimaryFileExtension(), ClipboardFormats.getFileExtensionArray()); @@ -249,10 +251,12 @@ public void share(Actor actor, LocalSession session, @CommandPermissions("worldedit.schematic.delete") public void delete(Actor actor, @Arg(desc = "File name.") - String filename) throws WorldEditException { + Schematic schematic) throws WorldEditException { LocalConfiguration config = worldEdit.getConfiguration(); File dir = worldEdit.getWorkingDirectoryPath(config.saveDir).toFile(); + // Schematic.path is relative, so treat it as filename + String filename = schematic.getPath().toString(); File f = worldEdit.getSafeOpenFile(actor, dir, filename, "schematic", ClipboardFormats.getFileExtensionArray()); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java new file mode 100644 index 0000000000..9a8d65df55 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java @@ -0,0 +1,89 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.command.argument; + +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.internal.util.LogManagerCompat; +import com.sk89q.worldedit.util.formatting.text.Component; +import com.sk89q.worldedit.util.formatting.text.TextComponent; +import com.sk89q.worldedit.util.io.file.FilenameException; +import com.sk89q.worldedit.util.schematic.Schematic; +import com.sk89q.worldedit.util.schematic.SchematicsManager; +import org.enginehub.piston.CommandManager; +import org.enginehub.piston.converter.ArgumentConverter; +import org.enginehub.piston.converter.ConversionResult; +import org.enginehub.piston.converter.FailedConversion; +import org.enginehub.piston.converter.SuccessfulConversion; +import org.enginehub.piston.inject.InjectedValueAccess; +import org.enginehub.piston.inject.Key; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.enginehub.piston.converter.SuggestionHelper.limitByPrefix; + +public class SchematicConverter implements ArgumentConverter { + + public static void register(WorldEdit worldEdit, CommandManager commandManager) { + commandManager.registerConverter(Key.of(Schematic.class), new SchematicConverter(worldEdit)); + } + + private final WorldEdit worldEdit; + + private SchematicConverter(WorldEdit worldEdit) { + this.worldEdit = worldEdit; + } + + private final TextComponent choices = TextComponent.of("schematic filename"); + + @Override + public Component describeAcceptableArguments() { + return choices; + } + + @Override + public List getSuggestions(String input, InjectedValueAccess context) { + SchematicsManager schematicsManager = worldEdit.getSchematicsManager(); + Path schematicsRootPath = schematicsManager.getRoot(); + + return limitByPrefix(schematicsManager.getList().stream() + .map(s -> schematicsRootPath.relativize(s.getPath()).toString()), input); + } + + @Override + public ConversionResult convert(String s, InjectedValueAccess injectedValueAccess) { + Path schematicsRoot = worldEdit.getSchematicsManager().getRoot(); + // resolve as subpath of schematicsRoot + Path schematicPath = schematicsRoot.resolve(s).toAbsolutePath(); + // then check whether it is still a subpath to rule out "../" + if (!schematicPath.startsWith(schematicsRoot)) { + return FailedConversion.from(new FilenameException(s)); + } + // check whether the file exists + if (Files.exists(schematicPath)) { + // continue as relative path to schematicsRoot + schematicPath = schematicsRoot.relativize(schematicPath); + return SuccessfulConversion.fromSingle(new Schematic(schematicPath)); + } else { + return FailedConversion.from(new FilenameException(s)); + } + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformCommandManager.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformCommandManager.java index 8a28f81fca..d801c0f1ca 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformCommandManager.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/PlatformCommandManager.java @@ -82,6 +82,7 @@ import com.sk89q.worldedit.command.argument.OffsetConverter; import com.sk89q.worldedit.command.argument.RegionFactoryConverter; import com.sk89q.worldedit.command.argument.RegistryConverter; +import com.sk89q.worldedit.command.argument.SchematicConverter; import com.sk89q.worldedit.command.argument.SelectorChoiceConverter; import com.sk89q.worldedit.command.argument.SideEffectConverter; import com.sk89q.worldedit.command.argument.SideEffectSetConverter; @@ -232,6 +233,7 @@ private void registerArgumentConverters() { ClipboardFormatConverter.register(commandManager); ClipboardShareDestinationConverter.register(commandManager); SelectorChoiceConverter.register(commandManager); + SchematicConverter.register(worldEdit, commandManager); } private void registerAlwaysInjectedValues() { From 0551ff6ea25c95ea3c5c16597216260bbc7a3357 Mon Sep 17 00:00:00 2001 From: Markus Ebner Date: Tue, 8 Nov 2022 23:24:43 +0100 Subject: [PATCH 03/10] Rename Schematic to SchematicPath and cleanup - Resolve first batch of review comments - Renamed Schematic to SchematicPath - Removed custom hashCode() and equals() implementations - Added unit-test to make sure it behaves as expected --- .../worldedit/command/SchematicCommands.java | 27 ++++----- .../command/argument/SchematicConverter.java | 13 ++--- .../{Schematic.java => SchematicPath.java} | 21 +------ .../util/schematic/SchematicsManager.java | 2 +- .../backends/DummySchematicsBackend.java | 5 +- .../FileWatcherSchematicsBackend.java | 12 ++-- .../backends/PollingSchematicsBackend.java | 12 ++-- .../schematic/backends/SchematicsBackend.java | 4 +- .../util/schematic/SchematicPathTest.java | 57 +++++++++++++++++++ 9 files changed, 93 insertions(+), 60 deletions(-) rename worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/{Schematic.java => SchematicPath.java} (69%) create mode 100644 worldedit-core/src/test/java/com/sk89q/worldedit/util/schematic/SchematicPathTest.java diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java index 566bee2bcf..de0c64d67b 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java @@ -55,7 +55,7 @@ import com.sk89q.worldedit.util.io.Closer; import com.sk89q.worldedit.util.io.file.FilenameException; import com.sk89q.worldedit.util.io.file.MorePaths; -import com.sk89q.worldedit.util.schematic.Schematic; +import com.sk89q.worldedit.util.schematic.SchematicPath; import com.sk89q.worldedit.util.schematic.SchematicsManager; import org.apache.logging.log4j.Logger; import org.enginehub.piston.annotation.Command; @@ -73,10 +73,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.concurrent.Callable; @@ -111,13 +108,13 @@ public SchematicCommands(WorldEdit worldEdit) { @CommandPermissions({"worldedit.clipboard.load", "worldedit.schematic.load"}) public void load(Actor actor, LocalSession session, @Arg(desc = "File name.") - Schematic schematic, + SchematicPath schematic, @Arg(desc = "Format name.", def = "sponge") ClipboardFormat format) throws FilenameException { LocalConfiguration config = worldEdit.getConfiguration(); // Schematic.path is relative, so treat it as filename - String filename = schematic.getPath().toString(); + String filename = schematic.path().toString(); File schematicsRoot = worldEdit.getSchematicsManager().getRoot().toFile(); File f = worldEdit.getSafeOpenFile(actor, schematicsRoot, filename, BuiltInClipboardFormat.SPONGE_V3_SCHEMATIC.getPrimaryFileExtension(), @@ -251,12 +248,12 @@ public void share(Actor actor, LocalSession session, @CommandPermissions("worldedit.schematic.delete") public void delete(Actor actor, @Arg(desc = "File name.") - Schematic schematic) throws WorldEditException { + SchematicPath schematic) throws WorldEditException { LocalConfiguration config = worldEdit.getConfiguration(); File dir = worldEdit.getWorkingDirectoryPath(config.saveDir).toFile(); // Schematic.path is relative, so treat it as filename - String filename = schematic.getPath().toString(); + String filename = schematic.path().toString(); File f = worldEdit.getSafeOpenFile(actor, dir, filename, "schematic", ClipboardFormats.getFileExtensionArray()); @@ -335,7 +332,7 @@ public void list(Actor actor, final String pageCommand = actor.isPlayer() ? "//schem list -p %page%" + flag : null; - Comparator schematicComparator = (s0, s1) -> pathComparator.compare(s0.getPath(), s1.getPath()); + Comparator schematicComparator = (s0, s1) -> pathComparator.compare(s0.path(), s1.path()); WorldEditAsyncCommandBuilder.createAndSendMessage(actor, new SchematicListTask(schematicComparator, page, pageCommand), @@ -445,11 +442,11 @@ public Consumer call() throws Exception { } private static class SchematicListTask implements Callable { - private final Comparator pathComparator; + private final Comparator pathComparator; private final int page; private final String pageCommand; - SchematicListTask(Comparator pathComparator, int page, String pageCommand) { + SchematicListTask(Comparator pathComparator, int page, String pageCommand) { this.pathComparator = pathComparator; this.page = page; this.pageCommand = pageCommand; @@ -458,7 +455,7 @@ private static class SchematicListTask implements Callable { @Override public Component call() throws Exception { SchematicsManager schematicsManager = WorldEdit.getInstance().getSchematicsManager(); - List fileList = schematicsManager.getList(); + List fileList = schematicsManager.getList(); if (fileList.isEmpty()) { return ErrorFormat.wrap("No schematics found."); @@ -473,9 +470,9 @@ public Component call() throws Exception { private static class SchematicPaginationBox extends PaginationBox { private final Path rootDir; - private final List files; + private final List files; - SchematicPaginationBox(Path rootDir, List files, String pageCommand) { + SchematicPaginationBox(Path rootDir, List files, String pageCommand) { super("Available schematics", pageCommand); this.rootDir = rootDir; this.files = files; @@ -484,7 +481,7 @@ private static class SchematicPaginationBox extends PaginationBox { @Override public Component getComponent(int number) { checkArgument(number < files.size() && number >= 0); - Path file = files.get(number).getPath(); + Path file = files.get(number).path(); String format = ClipboardFormats.getFileExtensionMap() .get(MoreFiles.getFileExtension(file)) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java index 9a8d65df55..65203e80c0 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java @@ -20,11 +20,10 @@ package com.sk89q.worldedit.command.argument; import com.sk89q.worldedit.WorldEdit; -import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.util.formatting.text.Component; import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.io.file.FilenameException; -import com.sk89q.worldedit.util.schematic.Schematic; +import com.sk89q.worldedit.util.schematic.SchematicPath; import com.sk89q.worldedit.util.schematic.SchematicsManager; import org.enginehub.piston.CommandManager; import org.enginehub.piston.converter.ArgumentConverter; @@ -40,10 +39,10 @@ import static org.enginehub.piston.converter.SuggestionHelper.limitByPrefix; -public class SchematicConverter implements ArgumentConverter { +public class SchematicConverter implements ArgumentConverter { public static void register(WorldEdit worldEdit, CommandManager commandManager) { - commandManager.registerConverter(Key.of(Schematic.class), new SchematicConverter(worldEdit)); + commandManager.registerConverter(Key.of(SchematicPath.class), new SchematicConverter(worldEdit)); } private final WorldEdit worldEdit; @@ -65,11 +64,11 @@ public List getSuggestions(String input, InjectedValueAccess context) { Path schematicsRootPath = schematicsManager.getRoot(); return limitByPrefix(schematicsManager.getList().stream() - .map(s -> schematicsRootPath.relativize(s.getPath()).toString()), input); + .map(s -> schematicsRootPath.relativize(s.path()).toString()), input); } @Override - public ConversionResult convert(String s, InjectedValueAccess injectedValueAccess) { + public ConversionResult convert(String s, InjectedValueAccess injectedValueAccess) { Path schematicsRoot = worldEdit.getSchematicsManager().getRoot(); // resolve as subpath of schematicsRoot Path schematicPath = schematicsRoot.resolve(s).toAbsolutePath(); @@ -81,7 +80,7 @@ public ConversionResult convert(String s, InjectedValueAccess injecte if (Files.exists(schematicPath)) { // continue as relative path to schematicsRoot schematicPath = schematicsRoot.relativize(schematicPath); - return SuccessfulConversion.fromSingle(new Schematic(schematicPath)); + return SuccessfulConversion.fromSingle(new SchematicPath(schematicPath)); } else { return FailedConversion.from(new FilenameException(s)); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/Schematic.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicPath.java similarity index 69% rename from worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/Schematic.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicPath.java index 69317c2eb8..1e0bf109cd 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/Schematic.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicPath.java @@ -24,30 +24,11 @@ /** * Record representing one Schematic file. */ -public record Schematic(Path path) { - - /** - * Get this Schematic's path. - * @return This Schematic's path. - */ - public Path getPath() { - return path; - } +public record SchematicPath(Path path) { @Override public String toString() { return path.toString(); } - @Override - public int hashCode() { - return path.hashCode(); - } - - @Override - public boolean equals(Object obj) { - Schematic other = (Schematic) obj; - if (other == null) { return false; } - return path.equals(other.getPath()); - } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java index c1adc4cd8e..6a7db8498e 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java @@ -102,7 +102,7 @@ public Path getRoot() { * Get a list of all known schematics. * @return List of all known schematics. */ - public List getList() { + public List getList() { return backend.getList(); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java index 7594b1026c..17cf24e61a 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java @@ -19,9 +19,8 @@ package com.sk89q.worldedit.util.schematic.backends; -import com.sk89q.worldedit.util.schematic.Schematic; +import com.sk89q.worldedit.util.schematic.SchematicPath; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -39,7 +38,7 @@ public void uninit() { } @Override - public List getList() { + public List getList() { return new ArrayList<>(); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java index 038edb09e0..ba9a294452 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java @@ -21,7 +21,7 @@ import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.util.io.file.RecursiveDirectoryWatcher; -import com.sk89q.worldedit.util.schematic.Schematic; +import com.sk89q.worldedit.util.schematic.SchematicPath; import org.apache.logging.log4j.Logger; import java.io.IOException; @@ -41,7 +41,7 @@ public class FileWatcherSchematicsBackend implements SchematicsBackend { private static final Logger LOGGER = LogManagerCompat.getLogger(); private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - private final Set schematics = new HashSet<>(); + private final Set schematics = new HashSet<>(); private final RecursiveDirectoryWatcher directoryWatcher; private FileWatcherSchematicsBackend(RecursiveDirectoryWatcher directoryWatcher) { @@ -64,9 +64,9 @@ public void init() { directoryWatcher.start(event -> { lock.writeLock().lock(); if (event instanceof RecursiveDirectoryWatcher.FileCreatedEvent) { - schematics.add(new Schematic(event.getPath())); + schematics.add(new SchematicPath(event.getPath())); } else if (event instanceof RecursiveDirectoryWatcher.FileDeletedEvent) { - schematics.remove(new Schematic(event.getPath())); + schematics.remove(new SchematicPath(event.getPath())); } lock.writeLock().unlock(); if (event instanceof RecursiveDirectoryWatcher.FileCreatedEvent) { @@ -84,9 +84,9 @@ public void uninit() { } @Override - public List getList() { + public List getList() { lock.readLock().lock(); - List result = new ArrayList<>(schematics); + List result = new ArrayList<>(schematics); lock.readLock().unlock(); return result; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java index e47863c433..10f5ebc307 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java @@ -20,7 +20,7 @@ package com.sk89q.worldedit.util.schematic.backends; import com.sk89q.worldedit.internal.util.LogManagerCompat; -import com.sk89q.worldedit.util.schematic.Schematic; +import com.sk89q.worldedit.util.schematic.SchematicPath; import org.apache.logging.log4j.Logger; import java.io.IOException; @@ -46,7 +46,7 @@ public class PollingSchematicsBackend implements SchematicsBackend { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final Path schematicsDir; private Instant lastUpdateTs = Instant.EPOCH; - private List schematics = new ArrayList<>(); + private List schematics = new ArrayList<>(); private PollingSchematicsBackend(Path schematicsDir) { this.schematicsDir = schematicsDir; @@ -61,14 +61,14 @@ public static PollingSchematicsBackend create(Path schematicsFolder) { return new PollingSchematicsBackend(schematicsFolder); } - private List scanFolder(Path root) { - List pathList = new ArrayList<>(); + private List scanFolder(Path root) { + List pathList = new ArrayList<>(); try (DirectoryStream stream = Files.newDirectoryStream(root)) { for (Path path : stream) { if (Files.isDirectory(path)) { pathList.addAll(scanFolder(path)); } else { - pathList.add(new Schematic(path)); + pathList.add(new SchematicPath(path)); } } } catch (IOException e) { @@ -94,7 +94,7 @@ public void uninit() { } @Override - public synchronized List getList() { + public synchronized List getList() { // udpate internal cache if requried (determined by age) Duration age = Duration.between(lastUpdateTs, Instant.now()); if (age.compareTo(MAX_RESULT_AGE) >= 0) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java index 46c1f111b2..4c7a35b803 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java @@ -19,7 +19,7 @@ package com.sk89q.worldedit.util.schematic.backends; -import com.sk89q.worldedit.util.schematic.Schematic; +import com.sk89q.worldedit.util.schematic.SchematicPath; import java.util.List; @@ -43,7 +43,7 @@ public interface SchematicsBackend { * * @return List of known schematics. */ - List getList(); + List getList(); /** * Tells the backend that there are changes it should take into account. diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/util/schematic/SchematicPathTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/util/schematic/SchematicPathTest.java new file mode 100644 index 0000000000..a992754724 --- /dev/null +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/util/schematic/SchematicPathTest.java @@ -0,0 +1,57 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.util.schematic; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests {@link SchematicPath}. + * + * @see + * Trusting me4502 is good, controlling is better! :) + * + */ +public class SchematicPathTest { + + @Test + public void testHashAndEquality() { + Path p0 = Path.of("/tmp/testpath0"); + Path p1 = Path.of("/tmp/testpath1"); + Path p0equiv = Path.of("/tmp/testpath0"); + + SchematicPath s0 = new SchematicPath(p0); + SchematicPath s1 = new SchematicPath(p1); + SchematicPath s0equiv = new SchematicPath(p0equiv); + + assertEquals(s0.hashCode(), s0equiv.hashCode()); + assertNotEquals(s0.hashCode(), s1.hashCode()); + + assertTrue(s0.equals(s0equiv)); + assertFalse(s0.equals(s1)); + } + +} From efb6304814e56f6ed46b54212e0703b500e2fadb Mon Sep 17 00:00:00 2001 From: Markus Ebner Date: Fri, 11 Nov 2022 13:56:36 +0100 Subject: [PATCH 04/10] Remove unncessary (un)init log points --- .../util/schematic/backends/FileWatcherSchematicsBackend.java | 1 - .../util/schematic/backends/PollingSchematicsBackend.java | 2 -- 2 files changed, 3 deletions(-) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java index ba9a294452..d07fcd9c93 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java @@ -79,7 +79,6 @@ public void init() { @Override public void uninit() { - LOGGER.debug("uninit"); directoryWatcher.stop(); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java index 10f5ebc307..5c66553d50 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java @@ -85,12 +85,10 @@ private void runRescan() { @Override public void init() { - LOGGER.debug("init"); } @Override public void uninit() { - LOGGER.debug("uninit"); } @Override From 64d8e3996943ccbbf8bfe8f44277bba2f881f2db Mon Sep 17 00:00:00 2001 From: Maddy Miller Date: Sun, 21 May 2023 16:28:38 +1000 Subject: [PATCH 05/10] Apply suggestions from code review Co-authored-by: Octavia Togami --- .../io/file/RecursiveDirectoryWatcher.java | 12 ++++----- .../util/schematic/SchematicsManager.java | 22 ++++++---------- .../backends/DummySchematicsBackend.java | 3 +-- .../FileWatcherSchematicsBackend.java | 25 +++++++++++-------- .../backends/PollingSchematicsBackend.java | 7 +++--- .../schematic/backends/SchematicsBackend.java | 10 +++----- 6 files changed, 36 insertions(+), 43 deletions(-) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/RecursiveDirectoryWatcher.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/RecursiveDirectoryWatcher.java index 849b816967..4931326de7 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/RecursiveDirectoryWatcher.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/RecursiveDirectoryWatcher.java @@ -35,15 +35,15 @@ import java.util.function.Consumer; /** - * Helper class that allows to recursively monitor a directory for changed (created / deleted) files / folders. + * Helper class that recursively monitors a directory for changes to files and folders, including creation, deletion, and modification. * - * @warning File- and Folder events might be sent multiple times. Users of this class need to employ their own + * @warning File and folder events might be sent multiple times. Users of this class need to employ their own * deduplication! */ public class RecursiveDirectoryWatcher { /** - * Base-Class for all DirEntry change events. + * Base class for all change events. */ public static class DirEntryChangeEvent { private Path path; @@ -109,10 +109,10 @@ private RecursiveDirectoryWatcher(Path root, WatchService watchService) { /** * Create a new recursive directory watcher for the given root folder. - * You have to call @see start() before the instance starts monitoring. + * You have to call {@link #start()} before the instance starts monitoring. * * @param root Folder to watch for changed files recursively. - * @return A new RecursiveDirectoryWatcher instance, monitoring the given root folder. + * @return a new instance that will monitor the given root folder * @throws IOException If creating the watcher failed, e.g. due to root not existing. */ public static RecursiveDirectoryWatcher create(Path root) throws IOException { @@ -159,7 +159,7 @@ public void start(Consumer eventConsumer) { for (WatchEvent event : watchKey.pollEvents()) { WatchEvent.Kind kind = event.kind(); if (kind.equals(StandardWatchEventKinds.OVERFLOW)) { - LOGGER.warn("RecursiveDirectoryWatcher Seems like we can't keep up with updates"); + LOGGER.warn("Seems like we can't keep up with updates"); continue; } // make sure to work with an absolute path diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java index 6a7db8498e..3c9eb09a40 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java @@ -33,14 +33,12 @@ import java.util.List; /** - * Class that manages the known Schematic files. + * Class that manages the known schematic files. * - *

This class monitors the schematics folder for changes and keeps an always-up-to-date list of known - * schematics in RAM in order to speed up queries. - * This further also allows more convenient features like supporting command suggestions for known schematics. + *

This class monitors the schematics folder for changes and maintains an up-to-date list of known + * schematics in order to speed up queries. * - *

If initialization of the inotify Backend fails, SchematicsManager is going to fall back to a polled variant, where - * the result is cached for a certain amount of time, before a rescan is performed. (Eventual Consistency Cache) + *

If initialization of the file-watching backend fails, a polling backend is used instead. */ public class SchematicsManager { @@ -58,7 +56,7 @@ private void setupBackend() { try { backend = FileWatcherSchematicsBackend.create(schematicsDir); } catch (IOException e) { - LOGGER.warn("Failed to initialize folder-monitoring based Schematics backend. Falling back to scanning."); + LOGGER.warn("Failed to initialize file-monitoring based schematics backend. Falling back to polling.", e); backend = PollingSchematicsBackend.create(schematicsDir); } } @@ -91,25 +89,21 @@ public void uninit() { } /** - * Get the RootPath for schematics. - * @return The root folder where schematics are stored. + * {@return the root folder where schematics are stored} */ public Path getRoot() { return schematicsDir; } /** - * Get a list of all known schematics. - * @return List of all known schematics. + * {@return a list of all known schematics} */ public List getList() { return backend.getList(); } /** - * Tell SchematicManager that an update is in order. - * This should be used whenever WorldEdit code adds a new Schematic, to make sure the next list - * response is up-to-date for better user-experience. + * Force an update of the list. */ public void update() { backend.update(); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java index 17cf24e61a..70aacaeedd 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java @@ -25,8 +25,7 @@ import java.util.List; /** - * DummyBackend for the SchematicsManager. - * This is only used in the error-case. For example when creating the schematics folder failed for whatever reason. + * A backend that never lists any files. */ public class DummySchematicsBackend implements SchematicsBackend { @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java index d07fcd9c93..2b84171da7 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java @@ -33,9 +33,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; /** - * SchematicsBackend making use of the RecursiveDirectoryWatcher. - * This backend initially scans all schematics in the folder tree and then registers for file change events to - * avoid manually polling / rescanning the folder structure for every query. + * A backend that efficiently scans for file changes using {@link RecursiveDirectoryWatcher}. */ public class FileWatcherSchematicsBackend implements SchematicsBackend { private static final Logger LOGGER = LogManagerCompat.getLogger(); @@ -63,12 +61,15 @@ public static FileWatcherSchematicsBackend create(Path schematicsFolder) throws public void init() { directoryWatcher.start(event -> { lock.writeLock().lock(); - if (event instanceof RecursiveDirectoryWatcher.FileCreatedEvent) { - schematics.add(new SchematicPath(event.getPath())); - } else if (event instanceof RecursiveDirectoryWatcher.FileDeletedEvent) { - schematics.remove(new SchematicPath(event.getPath())); + try { + if (event instanceof RecursiveDirectoryWatcher.FileCreatedEvent) { + schematics.add(new SchematicPath(event.getPath())); + } else if (event instanceof RecursiveDirectoryWatcher.FileDeletedEvent) { + schematics.remove(new SchematicPath(event.getPath())); + } + } finally { + lock.writeLock().unlock(); } - lock.writeLock().unlock(); if (event instanceof RecursiveDirectoryWatcher.FileCreatedEvent) { LOGGER.info("New Schematic found: " + event.getPath()); } else if (event instanceof RecursiveDirectoryWatcher.FileDeletedEvent) { @@ -85,9 +86,11 @@ public void uninit() { @Override public List getList() { lock.readLock().lock(); - List result = new ArrayList<>(schematics); - lock.readLock().unlock(); - return result; + try { + return List.copyOf(schematics); + } finally { + lock.readLock().unlock(); + } } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java index 5c66553d50..392cb06725 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java @@ -34,8 +34,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; /** - * SchematicsBackend implementation that scans the folder tree, then caches the result for a certain amount of time. - * This essentially is an eventually consistent cache that is used as fallback. + * A backend that scans the folder tree, then caches the result for a certain amount of time. */ public class PollingSchematicsBackend implements SchematicsBackend { @@ -78,7 +77,7 @@ private List scanFolder(Path root) { } private void runRescan() { - LOGGER.debug("Rescanning Schematics"); + LOGGER.debug("Rescanning schematics"); this.schematics = scanFolder(schematicsDir); lastUpdateTs = Instant.now(); } @@ -98,7 +97,7 @@ public synchronized List getList() { if (age.compareTo(MAX_RESULT_AGE) >= 0) { runRescan(); } - return new ArrayList<>(schematics); + return List.copyOf(schematics); } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java index 4c7a35b803..33665bbd57 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java @@ -24,24 +24,22 @@ import java.util.List; /** - * SchematicsManager backend interface. + * {@link SchematicsManager} backend interface. */ public interface SchematicsBackend { /** - * Initialize the SchematicsManager backend. + * Initialize the backend. */ void init(); /** - * Uninitialize the SchematicsManager backend. + * Uninitialize the backend. */ void uninit(); /** - * Get the list of known schematics. - * - * @return List of known schematics. + * {@return the list of known schematics} */ List getList(); From 4f3ad0ce8a46506b29463ada4c7152e5aa1d588b Mon Sep 17 00:00:00 2001 From: Madeline Miller Date: Sun, 21 May 2023 16:54:14 +1000 Subject: [PATCH 06/10] Resolve many of octy's review notes --- .../java/com/sk89q/worldedit/WorldEdit.java | 2 +- .../worldedit/command/SchematicCommands.java | 30 +++++----- .../command/argument/SchematicConverter.java | 14 ++--- .../annotation}/SchematicPath.java | 22 +++---- .../schematic/SchematicsManager.java | 31 +++++++--- .../backends/DummySchematicsBackend.java | 9 +-- .../FileWatcherSchematicsBackend.java | 24 ++++---- .../backends/PollingSchematicsBackend.java | 13 ++--- .../schematic/backends/SchematicsBackend.java | 8 ++- .../util}/RecursiveDirectoryWatcher.java | 56 +++++++----------- .../util/schematic/SchematicPathTest.java | 57 ------------------- 11 files changed, 104 insertions(+), 162 deletions(-) rename worldedit-core/src/main/java/com/sk89q/worldedit/{util/schematic => internal/annotation}/SchematicPath.java (65%) rename worldedit-core/src/main/java/com/sk89q/worldedit/{util => internal}/schematic/SchematicsManager.java (73%) rename worldedit-core/src/main/java/com/sk89q/worldedit/{util => internal}/schematic/backends/DummySchematicsBackend.java (84%) rename worldedit-core/src/main/java/com/sk89q/worldedit/{util => internal}/schematic/backends/FileWatcherSchematicsBackend.java (77%) rename worldedit-core/src/main/java/com/sk89q/worldedit/{util => internal}/schematic/backends/PollingSchematicsBackend.java (88%) rename worldedit-core/src/main/java/com/sk89q/worldedit/{util => internal}/schematic/backends/SchematicsBackend.java (83%) rename worldedit-core/src/main/java/com/sk89q/worldedit/{util/io/file => internal/util}/RecursiveDirectoryWatcher.java (84%) delete mode 100644 worldedit-core/src/test/java/com/sk89q/worldedit/util/schematic/SchematicPathTest.java diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java b/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java index abe09f29b6..d2a0c6400b 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java @@ -64,7 +64,7 @@ import com.sk89q.worldedit.util.io.file.FilenameException; import com.sk89q.worldedit.util.io.file.FilenameResolutionException; import com.sk89q.worldedit.util.io.file.InvalidFilenameException; -import com.sk89q.worldedit.util.schematic.SchematicsManager; +import com.sk89q.worldedit.internal.schematic.SchematicsManager; import com.sk89q.worldedit.util.task.SimpleSupervisor; import com.sk89q.worldedit.util.task.Supervisor; import com.sk89q.worldedit.util.translation.TranslationManager; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java index de0c64d67b..fec94c518f 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java @@ -55,8 +55,8 @@ import com.sk89q.worldedit.util.io.Closer; import com.sk89q.worldedit.util.io.file.FilenameException; import com.sk89q.worldedit.util.io.file.MorePaths; -import com.sk89q.worldedit.util.schematic.SchematicPath; -import com.sk89q.worldedit.util.schematic.SchematicsManager; +import com.sk89q.worldedit.internal.annotation.SchematicPath; +import com.sk89q.worldedit.internal.schematic.SchematicsManager; import org.apache.logging.log4j.Logger; import org.enginehub.piston.annotation.Command; import org.enginehub.piston.annotation.CommandContainer; @@ -107,14 +107,15 @@ public SchematicCommands(WorldEdit worldEdit) { ) @CommandPermissions({"worldedit.clipboard.load", "worldedit.schematic.load"}) public void load(Actor actor, LocalSession session, + @SchematicPath @Arg(desc = "File name.") - SchematicPath schematic, + Path schematic, @Arg(desc = "Format name.", def = "sponge") ClipboardFormat format) throws FilenameException { LocalConfiguration config = worldEdit.getConfiguration(); // Schematic.path is relative, so treat it as filename - String filename = schematic.path().toString(); + String filename = schematic.toString(); File schematicsRoot = worldEdit.getSchematicsManager().getRoot().toFile(); File f = worldEdit.getSafeOpenFile(actor, schematicsRoot, filename, BuiltInClipboardFormat.SPONGE_V3_SCHEMATIC.getPrimaryFileExtension(), @@ -247,13 +248,14 @@ public void share(Actor actor, LocalSession session, ) @CommandPermissions("worldedit.schematic.delete") public void delete(Actor actor, + @SchematicPath @Arg(desc = "File name.") - SchematicPath schematic) throws WorldEditException { + Path schematic) throws WorldEditException { LocalConfiguration config = worldEdit.getConfiguration(); File dir = worldEdit.getWorkingDirectoryPath(config.saveDir).toFile(); // Schematic.path is relative, so treat it as filename - String filename = schematic.path().toString(); + String filename = schematic.toString(); File f = worldEdit.getSafeOpenFile(actor, dir, filename, "schematic", ClipboardFormats.getFileExtensionArray()); @@ -332,10 +334,8 @@ public void list(Actor actor, final String pageCommand = actor.isPlayer() ? "//schem list -p %page%" + flag : null; - Comparator schematicComparator = (s0, s1) -> pathComparator.compare(s0.path(), s1.path()); - WorldEditAsyncCommandBuilder.createAndSendMessage(actor, - new SchematicListTask(schematicComparator, page, pageCommand), + new SchematicListTask(pathComparator::compare, page, pageCommand), SubtleFormat.wrap("(Please wait... gathering schematic list.)")); } @@ -442,11 +442,11 @@ public Consumer call() throws Exception { } private static class SchematicListTask implements Callable { - private final Comparator pathComparator; + private final Comparator pathComparator; private final int page; private final String pageCommand; - SchematicListTask(Comparator pathComparator, int page, String pageCommand) { + SchematicListTask(Comparator pathComparator, int page, String pageCommand) { this.pathComparator = pathComparator; this.page = page; this.pageCommand = pageCommand; @@ -455,7 +455,7 @@ private static class SchematicListTask implements Callable { @Override public Component call() throws Exception { SchematicsManager schematicsManager = WorldEdit.getInstance().getSchematicsManager(); - List fileList = schematicsManager.getList(); + List fileList = schematicsManager.getList(); if (fileList.isEmpty()) { return ErrorFormat.wrap("No schematics found."); @@ -470,9 +470,9 @@ public Component call() throws Exception { private static class SchematicPaginationBox extends PaginationBox { private final Path rootDir; - private final List files; + private final List files; - SchematicPaginationBox(Path rootDir, List files, String pageCommand) { + SchematicPaginationBox(Path rootDir, List files, String pageCommand) { super("Available schematics", pageCommand); this.rootDir = rootDir; this.files = files; @@ -481,7 +481,7 @@ private static class SchematicPaginationBox extends PaginationBox { @Override public Component getComponent(int number) { checkArgument(number < files.size() && number >= 0); - Path file = files.get(number).path(); + Path file = files.get(number); String format = ClipboardFormats.getFileExtensionMap() .get(MoreFiles.getFileExtension(file)) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java index 65203e80c0..3d86de222c 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java @@ -23,8 +23,8 @@ import com.sk89q.worldedit.util.formatting.text.Component; import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.io.file.FilenameException; -import com.sk89q.worldedit.util.schematic.SchematicPath; -import com.sk89q.worldedit.util.schematic.SchematicsManager; +import com.sk89q.worldedit.internal.annotation.SchematicPath; +import com.sk89q.worldedit.internal.schematic.SchematicsManager; import org.enginehub.piston.CommandManager; import org.enginehub.piston.converter.ArgumentConverter; import org.enginehub.piston.converter.ConversionResult; @@ -39,10 +39,10 @@ import static org.enginehub.piston.converter.SuggestionHelper.limitByPrefix; -public class SchematicConverter implements ArgumentConverter { +public class SchematicConverter implements ArgumentConverter { public static void register(WorldEdit worldEdit, CommandManager commandManager) { - commandManager.registerConverter(Key.of(SchematicPath.class), new SchematicConverter(worldEdit)); + commandManager.registerConverter(Key.of(Path.class, SchematicPath.class), new SchematicConverter(worldEdit)); } private final WorldEdit worldEdit; @@ -64,11 +64,11 @@ public List getSuggestions(String input, InjectedValueAccess context) { Path schematicsRootPath = schematicsManager.getRoot(); return limitByPrefix(schematicsManager.getList().stream() - .map(s -> schematicsRootPath.relativize(s.path()).toString()), input); + .map(s -> schematicsRootPath.relativize(s).toString()), input); } @Override - public ConversionResult convert(String s, InjectedValueAccess injectedValueAccess) { + public ConversionResult convert(String s, InjectedValueAccess injectedValueAccess) { Path schematicsRoot = worldEdit.getSchematicsManager().getRoot(); // resolve as subpath of schematicsRoot Path schematicPath = schematicsRoot.resolve(s).toAbsolutePath(); @@ -80,7 +80,7 @@ public ConversionResult convert(String s, InjectedValueAccess inj if (Files.exists(schematicPath)) { // continue as relative path to schematicsRoot schematicPath = schematicsRoot.relativize(schematicPath); - return SuccessfulConversion.fromSingle(new SchematicPath(schematicPath)); + return SuccessfulConversion.fromSingle(schematicPath); } else { return FailedConversion.from(new FilenameException(s)); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicPath.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/annotation/SchematicPath.java similarity index 65% rename from worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicPath.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/annotation/SchematicPath.java index 1e0bf109cd..1cb691cfa8 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicPath.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/annotation/SchematicPath.java @@ -17,18 +17,20 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.util.schematic; +package com.sk89q.worldedit.internal.annotation; -import java.nio.file.Path; +import org.enginehub.piston.inject.InjectAnnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** - * Record representing one Schematic file. + * Annotation to denote an argument as a schematic path. */ -public record SchematicPath(Path path) { - - @Override - public String toString() { - return path.toString(); - } - +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +@InjectAnnotation +public @interface SchematicPath { } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/SchematicsManager.java similarity index 73% rename from worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/SchematicsManager.java index 3c9eb09a40..aafa226cf8 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/SchematicsManager.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/SchematicsManager.java @@ -17,14 +17,14 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.util.schematic; +package com.sk89q.worldedit.internal.schematic; import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.internal.schematic.backends.DummySchematicsBackend; +import com.sk89q.worldedit.internal.schematic.backends.FileWatcherSchematicsBackend; +import com.sk89q.worldedit.internal.schematic.backends.PollingSchematicsBackend; +import com.sk89q.worldedit.internal.schematic.backends.SchematicsBackend; import com.sk89q.worldedit.internal.util.LogManagerCompat; -import com.sk89q.worldedit.util.schematic.backends.DummySchematicsBackend; -import com.sk89q.worldedit.util.schematic.backends.FileWatcherSchematicsBackend; -import com.sk89q.worldedit.util.schematic.backends.PollingSchematicsBackend; -import com.sk89q.worldedit.util.schematic.backends.SchematicsBackend; import org.apache.logging.log4j.Logger; import java.io.IOException; @@ -32,6 +32,8 @@ import java.nio.file.Path; import java.util.List; +import static com.google.common.base.Preconditions.checkNotNull; + /** * Class that manages the known schematic files. * @@ -52,12 +54,21 @@ public SchematicsManager(WorldEdit worldEdit) { this.worldEdit = worldEdit; } + private void createFallbackBackend() { + LOGGER.warn("Failed to initialize file-monitoring based schematics backend. Falling back to polling."); + backend = PollingSchematicsBackend.create(schematicsDir); + } + private void setupBackend() { try { - backend = FileWatcherSchematicsBackend.create(schematicsDir); + var fileWatcherBackend = FileWatcherSchematicsBackend.create(schematicsDir); + if (fileWatcherBackend.isPresent()) { + backend = fileWatcherBackend.get(); + } else { + createFallbackBackend(); + } } catch (IOException e) { - LOGGER.warn("Failed to initialize file-monitoring based schematics backend. Falling back to polling.", e); - backend = PollingSchematicsBackend.create(schematicsDir); + createFallbackBackend(); } } @@ -92,13 +103,15 @@ public void uninit() { * {@return the root folder where schematics are stored} */ public Path getRoot() { + checkNotNull(schematicsDir, "not initialized"); return schematicsDir; } /** * {@return a list of all known schematics} */ - public List getList() { + public List getList() { + checkNotNull(backend); return backend.getList(); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/DummySchematicsBackend.java similarity index 84% rename from worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/DummySchematicsBackend.java index 70aacaeedd..583b945099 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/DummySchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/DummySchematicsBackend.java @@ -17,10 +17,11 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.util.schematic.backends; +package com.sk89q.worldedit.internal.schematic.backends; -import com.sk89q.worldedit.util.schematic.SchematicPath; +import com.sk89q.worldedit.internal.annotation.SchematicPath; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -37,8 +38,8 @@ public void uninit() { } @Override - public List getList() { - return new ArrayList<>(); + public List getList() { + return List.of(); } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java similarity index 77% rename from worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java index 2b84171da7..24dfcec818 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/FileWatcherSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java @@ -17,18 +17,17 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.util.schematic.backends; +package com.sk89q.worldedit.internal.schematic.backends; import com.sk89q.worldedit.internal.util.LogManagerCompat; -import com.sk89q.worldedit.util.io.file.RecursiveDirectoryWatcher; -import com.sk89q.worldedit.util.schematic.SchematicPath; +import com.sk89q.worldedit.internal.util.RecursiveDirectoryWatcher; import org.apache.logging.log4j.Logger; import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -39,7 +38,7 @@ public class FileWatcherSchematicsBackend implements SchematicsBackend { private static final Logger LOGGER = LogManagerCompat.getLogger(); private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - private final Set schematics = new HashSet<>(); + private final Set schematics = new HashSet<>(); private final RecursiveDirectoryWatcher directoryWatcher; private FileWatcherSchematicsBackend(RecursiveDirectoryWatcher directoryWatcher) { @@ -52,9 +51,8 @@ private FileWatcherSchematicsBackend(RecursiveDirectoryWatcher directoryWatcher) * @return A new FileWatcherSchematicsBackend instance. * @throws IOException When creation of the filesystem watcher fails. */ - public static FileWatcherSchematicsBackend create(Path schematicsFolder) throws IOException { - RecursiveDirectoryWatcher watcher = RecursiveDirectoryWatcher.create(schematicsFolder); - return new FileWatcherSchematicsBackend(watcher); + public static Optional create(Path schematicsFolder) throws IOException { + return RecursiveDirectoryWatcher.create(schematicsFolder).map(FileWatcherSchematicsBackend::new); } @Override @@ -63,17 +61,17 @@ public void init() { lock.writeLock().lock(); try { if (event instanceof RecursiveDirectoryWatcher.FileCreatedEvent) { - schematics.add(new SchematicPath(event.getPath())); + schematics.add(event.path()); } else if (event instanceof RecursiveDirectoryWatcher.FileDeletedEvent) { - schematics.remove(new SchematicPath(event.getPath())); + schematics.remove(event.path()); } } finally { lock.writeLock().unlock(); } if (event instanceof RecursiveDirectoryWatcher.FileCreatedEvent) { - LOGGER.info("New Schematic found: " + event.getPath()); + LOGGER.info("New Schematic found: " + event.path()); } else if (event instanceof RecursiveDirectoryWatcher.FileDeletedEvent) { - LOGGER.info("Schematic deleted: " + event.getPath()); + LOGGER.info("Schematic deleted: " + event.path()); } }); } @@ -84,7 +82,7 @@ public void uninit() { } @Override - public List getList() { + public List getList() { lock.readLock().lock(); try { return List.copyOf(schematics); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java similarity index 88% rename from worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java index 392cb06725..72115d522a 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/PollingSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java @@ -17,10 +17,9 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.util.schematic.backends; +package com.sk89q.worldedit.internal.schematic.backends; import com.sk89q.worldedit.internal.util.LogManagerCompat; -import com.sk89q.worldedit.util.schematic.SchematicPath; import org.apache.logging.log4j.Logger; import java.io.IOException; @@ -45,7 +44,7 @@ public class PollingSchematicsBackend implements SchematicsBackend { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final Path schematicsDir; private Instant lastUpdateTs = Instant.EPOCH; - private List schematics = new ArrayList<>(); + private List schematics = new ArrayList<>(); private PollingSchematicsBackend(Path schematicsDir) { this.schematicsDir = schematicsDir; @@ -60,14 +59,14 @@ public static PollingSchematicsBackend create(Path schematicsFolder) { return new PollingSchematicsBackend(schematicsFolder); } - private List scanFolder(Path root) { - List pathList = new ArrayList<>(); + private List scanFolder(Path root) { + List pathList = new ArrayList<>(); try (DirectoryStream stream = Files.newDirectoryStream(root)) { for (Path path : stream) { if (Files.isDirectory(path)) { pathList.addAll(scanFolder(path)); } else { - pathList.add(new SchematicPath(path)); + pathList.add(path); } } } catch (IOException e) { @@ -91,7 +90,7 @@ public void uninit() { } @Override - public synchronized List getList() { + public synchronized List getList() { // udpate internal cache if requried (determined by age) Duration age = Duration.between(lastUpdateTs, Instant.now()); if (age.compareTo(MAX_RESULT_AGE) >= 0) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/SchematicsBackend.java similarity index 83% rename from worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/SchematicsBackend.java index 33665bbd57..746510eb33 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/schematic/backends/SchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/SchematicsBackend.java @@ -17,10 +17,12 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.util.schematic.backends; +package com.sk89q.worldedit.internal.schematic.backends; -import com.sk89q.worldedit.util.schematic.SchematicPath; +import com.sk89q.worldedit.internal.annotation.SchematicPath; +import com.sk89q.worldedit.internal.schematic.SchematicsManager; +import java.nio.file.Path; import java.util.List; /** @@ -41,7 +43,7 @@ public interface SchematicsBackend { /** * {@return the list of known schematics} */ - List getList(); + List getList(); /** * Tells the backend that there are changes it should take into account. diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/RecursiveDirectoryWatcher.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java similarity index 84% rename from worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/RecursiveDirectoryWatcher.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java index 4931326de7..2d4be122f2 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/io/file/RecursiveDirectoryWatcher.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java @@ -17,11 +17,10 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.util.io.file; +package com.sk89q.worldedit.internal.util; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; -import com.sk89q.worldedit.internal.util.LogManagerCompat; import org.apache.logging.log4j.Logger; import java.io.IOException; @@ -32,65 +31,46 @@ import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; +import java.util.Optional; import java.util.function.Consumer; /** * Helper class that recursively monitors a directory for changes to files and folders, including creation, deletion, and modification. * - * @warning File and folder events might be sent multiple times. Users of this class need to employ their own + * @apiNote File and folder events might be sent multiple times. Users of this class need to employ their own * deduplication! */ public class RecursiveDirectoryWatcher { /** - * Base class for all change events. + * Base interface for all change events. */ - public static class DirEntryChangeEvent { - private Path path; - - public DirEntryChangeEvent(Path path) { - this.path = path; - } - - public Path getPath() { - return path; - } + public interface DirEntryChangeEvent { + Path path(); } /** * Event signaling the creation of a new file. */ - public static class FileCreatedEvent extends DirEntryChangeEvent { - public FileCreatedEvent(Path path) { - super(path); - } + public record FileCreatedEvent(Path path) implements DirEntryChangeEvent { } /** * Event signaling the deletion of a file. */ - public static class FileDeletedEvent extends DirEntryChangeEvent { - public FileDeletedEvent(Path path) { - super(path); - } + public record FileDeletedEvent(Path path) implements DirEntryChangeEvent { } /** * Event signaling the creation of a new directory. */ - public static class DirectoryCreatedEvent extends DirEntryChangeEvent { - public DirectoryCreatedEvent(Path path) { - super(path); - } + public record DirectoryCreatedEvent(Path path) implements DirEntryChangeEvent { } /** * Event signaling the deletion of a directory. */ - public static class DirectoryDeletedEvent extends DirEntryChangeEvent { - public DirectoryDeletedEvent(Path path) { - super(path); - } + public record DirectoryDeletedEvent(Path path) implements DirEntryChangeEvent { } @@ -109,19 +89,23 @@ private RecursiveDirectoryWatcher(Path root, WatchService watchService) { /** * Create a new recursive directory watcher for the given root folder. - * You have to call {@link #start()} before the instance starts monitoring. + * You have to call {@link #start(Consumer)} before the instance starts monitoring. * * @param root Folder to watch for changed files recursively. * @return a new instance that will monitor the given root folder * @throws IOException If creating the watcher failed, e.g. due to root not existing. */ - public static RecursiveDirectoryWatcher create(Path root) throws IOException { - WatchService watchService = root.getFileSystem().newWatchService(); - return new RecursiveDirectoryWatcher(root, watchService); + public static Optional create(Path root) throws IOException { + try { + WatchService watchService = root.getFileSystem().newWatchService(); + return Optional.of(new RecursiveDirectoryWatcher(root, watchService)); + } catch (UnsupportedOperationException ignored) { + return Optional.empty(); + } } private void registerFolderWatchAndScanInitially(Path root) throws IOException { - WatchKey watchKey = root.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE); + WatchKey watchKey = root.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); LOGGER.debug("Watch registered: " + root); watchRootMap.put(watchKey, root); eventConsumer.accept(new DirectoryCreatedEvent(root)); @@ -208,7 +192,7 @@ public void start(Consumer eventConsumer) { /** * Stop this RecursiveDirectoryWatcher instance and wait for it to be completely shut down. - * @warning RecursiveDirectoryWatcher is not reusable! + * @apiNote RecursiveDirectoryWatcher is not reusable! */ public void stop() { try { diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/util/schematic/SchematicPathTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/util/schematic/SchematicPathTest.java deleted file mode 100644 index a992754724..0000000000 --- a/worldedit-core/src/test/java/com/sk89q/worldedit/util/schematic/SchematicPathTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.util.schematic; - -import org.junit.jupiter.api.Test; - -import java.nio.file.Path; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Tests {@link SchematicPath}. - * - * @see - * Trusting me4502 is good, controlling is better! :) - * - */ -public class SchematicPathTest { - - @Test - public void testHashAndEquality() { - Path p0 = Path.of("/tmp/testpath0"); - Path p1 = Path.of("/tmp/testpath1"); - Path p0equiv = Path.of("/tmp/testpath0"); - - SchematicPath s0 = new SchematicPath(p0); - SchematicPath s1 = new SchematicPath(p1); - SchematicPath s0equiv = new SchematicPath(p0equiv); - - assertEquals(s0.hashCode(), s0equiv.hashCode()); - assertNotEquals(s0.hashCode(), s1.hashCode()); - - assertTrue(s0.equals(s0equiv)); - assertFalse(s0.equals(s1)); - } - -} From 0c7f5f60792796b8ca4216119262314b1550d2e6 Mon Sep 17 00:00:00 2001 From: Madeline Miller Date: Sun, 21 May 2023 17:07:06 +1000 Subject: [PATCH 07/10] Further review fixes --- .../backends/DummySchematicsBackend.java | 3 --- .../FileWatcherSchematicsBackend.java | 6 ++--- .../backends/PollingSchematicsBackend.java | 2 +- .../util/RecursiveDirectoryWatcher.java | 26 +++++++++++++------ 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/DummySchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/DummySchematicsBackend.java index 583b945099..87abc42f68 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/DummySchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/DummySchematicsBackend.java @@ -19,10 +19,7 @@ package com.sk89q.worldedit.internal.schematic.backends; -import com.sk89q.worldedit.internal.annotation.SchematicPath; - import java.nio.file.Path; -import java.util.ArrayList; import java.util.List; /** diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java index 24dfcec818..6b79e9fbf7 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java @@ -69,16 +69,16 @@ public void init() { lock.writeLock().unlock(); } if (event instanceof RecursiveDirectoryWatcher.FileCreatedEvent) { - LOGGER.info("New Schematic found: " + event.path()); + LOGGER.debug("New Schematic found: " + event.path()); } else if (event instanceof RecursiveDirectoryWatcher.FileDeletedEvent) { - LOGGER.info("Schematic deleted: " + event.path()); + LOGGER.debug("Schematic deleted: " + event.path()); } }); } @Override public void uninit() { - directoryWatcher.stop(); + directoryWatcher.close(); } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java index 72115d522a..25fb8088cf 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java @@ -70,7 +70,7 @@ private List scanFolder(Path root) { } } } catch (IOException e) { - e.printStackTrace(); + LOGGER.error(e); } return pathList; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java index 2d4be122f2..55a4c06f45 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java @@ -23,6 +23,7 @@ import com.google.common.collect.HashBiMap; import org.apache.logging.log4j.Logger; +import java.io.Closeable; import java.io.IOException; import java.nio.file.ClosedWatchServiceException; import java.nio.file.Files; @@ -40,7 +41,7 @@ * @apiNote File and folder events might be sent multiple times. Users of this class need to employ their own * deduplication! */ -public class RecursiveDirectoryWatcher { +public class RecursiveDirectoryWatcher implements Closeable { /** * Base interface for all change events. @@ -131,7 +132,9 @@ public void start(Consumer eventConsumer) { try { registerFolderWatchAndScanInitially(root); - } catch (IOException e) { e.printStackTrace(); } + } catch (IOException e) { + LOGGER.error(e); + } try { WatchKey watchKey; @@ -155,7 +158,9 @@ public void start(Consumer eventConsumer) { if (Files.isDirectory(path)) { // new subfolder created, create watch for it try { registerFolderWatchAndScanInitially(path); - } catch (IOException e) { e.printStackTrace(); } + } catch (IOException e) { + LOGGER.error(e); + } } else { // new file created eventConsumer.accept(new FileCreatedEvent(path)); } @@ -191,20 +196,25 @@ public void start(Consumer eventConsumer) { } /** - * Stop this RecursiveDirectoryWatcher instance and wait for it to be completely shut down. + * Close this RecursiveDirectoryWatcher instance and wait for it to be completely shut down. * @apiNote RecursiveDirectoryWatcher is not reusable! */ - public void stop() { + @Override + public void close() { try { watchService.close(); - } catch (IOException e) { e.printStackTrace(); } + } catch (IOException e) { + LOGGER.error(e); + } if (watchThread != null) { try { watchThread.join(); - } catch (InterruptedException e) { e.printStackTrace(); } + } catch (InterruptedException e) { + LOGGER.error(e); + } watchThread = null; } eventConsumer = null; } -} \ No newline at end of file +} From 92a3ad28afd89d71566512130f0a67d0d8b771ba Mon Sep 17 00:00:00 2001 From: Madeline Miller Date: Sun, 21 May 2023 19:52:35 +1000 Subject: [PATCH 08/10] Further review improvements --- .../java/com/sk89q/worldedit/WorldEdit.java | 2 +- .../worldedit/command/SchematicCommands.java | 8 ++++-- .../command/argument/SchematicConverter.java | 6 ++-- .../internal/schematic/SchematicsManager.java | 16 +++++++---- .../backends/DummySchematicsBackend.java | 6 ++-- .../FileWatcherSchematicsBackend.java | 5 ++-- .../backends/PollingSchematicsBackend.java | 15 ++++++---- .../schematic/backends/SchematicsBackend.java | 9 +++--- .../util/RecursiveDirectoryWatcher.java | 28 ++++++++++++++----- 9 files changed, 60 insertions(+), 35 deletions(-) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java b/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java index d2a0c6400b..c277f4d6ea 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java @@ -46,6 +46,7 @@ import com.sk89q.worldedit.function.pattern.Pattern; import com.sk89q.worldedit.internal.SchematicsEventListener; import com.sk89q.worldedit.internal.expression.invoke.ReturnException; +import com.sk89q.worldedit.internal.schematic.SchematicsManager; import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.scripting.CraftScriptContext; @@ -64,7 +65,6 @@ import com.sk89q.worldedit.util.io.file.FilenameException; import com.sk89q.worldedit.util.io.file.FilenameResolutionException; import com.sk89q.worldedit.util.io.file.InvalidFilenameException; -import com.sk89q.worldedit.internal.schematic.SchematicsManager; import com.sk89q.worldedit.util.task.SimpleSupervisor; import com.sk89q.worldedit.util.task.Supervisor; import com.sk89q.worldedit.util.translation.TranslationManager; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java index fec94c518f..199e951a1d 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java @@ -39,6 +39,8 @@ import com.sk89q.worldedit.extent.clipboard.io.ClipboardWriter; import com.sk89q.worldedit.extent.clipboard.io.share.ClipboardShareDestination; import com.sk89q.worldedit.extent.clipboard.io.share.ClipboardShareMetadata; +import com.sk89q.worldedit.internal.annotation.SchematicPath; +import com.sk89q.worldedit.internal.schematic.SchematicsManager; import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.math.transform.Transform; import com.sk89q.worldedit.session.ClipboardHolder; @@ -55,8 +57,6 @@ import com.sk89q.worldedit.util.io.Closer; import com.sk89q.worldedit.util.io.file.FilenameException; import com.sk89q.worldedit.util.io.file.MorePaths; -import com.sk89q.worldedit.internal.annotation.SchematicPath; -import com.sk89q.worldedit.internal.schematic.SchematicsManager; import org.apache.logging.log4j.Logger; import org.enginehub.piston.annotation.Command; import org.enginehub.piston.annotation.CommandContainer; @@ -74,6 +74,7 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.concurrent.Callable; @@ -455,7 +456,8 @@ private static class SchematicListTask implements Callable { @Override public Component call() throws Exception { SchematicsManager schematicsManager = WorldEdit.getInstance().getSchematicsManager(); - List fileList = schematicsManager.getList(); + // Copy this to a mutable list, we're sorting it below. + List fileList = new ArrayList<>(schematicsManager.getSchematicPaths()); if (fileList.isEmpty()) { return ErrorFormat.wrap("No schematics found."); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java index 3d86de222c..638ab8b05d 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/argument/SchematicConverter.java @@ -20,11 +20,11 @@ package com.sk89q.worldedit.command.argument; import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.internal.annotation.SchematicPath; +import com.sk89q.worldedit.internal.schematic.SchematicsManager; import com.sk89q.worldedit.util.formatting.text.Component; import com.sk89q.worldedit.util.formatting.text.TextComponent; import com.sk89q.worldedit.util.io.file.FilenameException; -import com.sk89q.worldedit.internal.annotation.SchematicPath; -import com.sk89q.worldedit.internal.schematic.SchematicsManager; import org.enginehub.piston.CommandManager; import org.enginehub.piston.converter.ArgumentConverter; import org.enginehub.piston.converter.ConversionResult; @@ -63,7 +63,7 @@ public List getSuggestions(String input, InjectedValueAccess context) { SchematicsManager schematicsManager = worldEdit.getSchematicsManager(); Path schematicsRootPath = schematicsManager.getRoot(); - return limitByPrefix(schematicsManager.getList().stream() + return limitByPrefix(schematicsManager.getSchematicPaths().stream() .map(s -> schematicsRootPath.relativize(s).toString()), input); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/SchematicsManager.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/SchematicsManager.java index aafa226cf8..b52122f34d 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/SchematicsManager.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/SchematicsManager.java @@ -30,7 +30,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; +import java.util.Set; import static com.google.common.base.Preconditions.checkNotNull; @@ -45,7 +45,7 @@ public class SchematicsManager { private static final Logger LOGGER = LogManagerCompat.getLogger(); - private WorldEdit worldEdit; + private final WorldEdit worldEdit; private Path schematicsDir; private SchematicsBackend backend; @@ -100,7 +100,9 @@ public void uninit() { } /** - * {@return the root folder where schematics are stored} + * Gets the root folder in which the schematics are stored. + * + * @return the root folder where schematics are stored */ public Path getRoot() { checkNotNull(schematicsDir, "not initialized"); @@ -108,11 +110,13 @@ public Path getRoot() { } /** - * {@return a list of all known schematics} + * Gets a set of known schematics. + * + * @return a set of all known schematics */ - public List getList() { + public Set getSchematicPaths() { checkNotNull(backend); - return backend.getList(); + return backend.getPaths(); } /** diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/DummySchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/DummySchematicsBackend.java index 87abc42f68..37964f30bd 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/DummySchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/DummySchematicsBackend.java @@ -20,7 +20,7 @@ package com.sk89q.worldedit.internal.schematic.backends; import java.nio.file.Path; -import java.util.List; +import java.util.Set; /** * A backend that never lists any files. @@ -35,8 +35,8 @@ public void uninit() { } @Override - public List getList() { - return List.of(); + public Set getPaths() { + return Set.of(); } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java index 6b79e9fbf7..59f2d8c838 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java @@ -26,7 +26,6 @@ import java.io.IOException; import java.nio.file.Path; import java.util.HashSet; -import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -82,10 +81,10 @@ public void uninit() { } @Override - public List getList() { + public Set getPaths() { lock.readLock().lock(); try { - return List.copyOf(schematics); + return Set.copyOf(schematics); } finally { lock.readLock().unlock(); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java index 25fb8088cf..05904a6dfd 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java @@ -19,7 +19,9 @@ package com.sk89q.worldedit.internal.schematic.backends; +import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.internal.util.LogManagerCompat; +import com.sk89q.worldedit.util.io.file.FilenameException; import org.apache.logging.log4j.Logger; import java.io.IOException; @@ -30,6 +32,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.concurrent.locks.ReentrantReadWriteLock; /** @@ -41,7 +44,6 @@ public class PollingSchematicsBackend implements SchematicsBackend { private static final Duration MAX_RESULT_AGE = Duration.ofSeconds(10); - private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final Path schematicsDir; private Instant lastUpdateTs = Instant.EPOCH; private List schematics = new ArrayList<>(); @@ -61,15 +63,18 @@ public static PollingSchematicsBackend create(Path schematicsFolder) { private List scanFolder(Path root) { List pathList = new ArrayList<>(); + Path schematicRoot = WorldEdit.getInstance().getSchematicsManager().getRoot(); + try (DirectoryStream stream = Files.newDirectoryStream(root)) { for (Path path : stream) { + path = WorldEdit.getInstance().getSafeOpenFile(null, schematicRoot.toFile(), path.toString(), null).toPath(); if (Files.isDirectory(path)) { pathList.addAll(scanFolder(path)); } else { pathList.add(path); } } - } catch (IOException e) { + } catch (IOException | FilenameException e) { LOGGER.error(e); } return pathList; @@ -90,13 +95,13 @@ public void uninit() { } @Override - public synchronized List getList() { - // udpate internal cache if requried (determined by age) + public synchronized Set getPaths() { + // Update internal cache if required (determined by age) Duration age = Duration.between(lastUpdateTs, Instant.now()); if (age.compareTo(MAX_RESULT_AGE) >= 0) { runRescan(); } - return List.copyOf(schematics); + return Set.copyOf(schematics); } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/SchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/SchematicsBackend.java index 746510eb33..6b1f88d253 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/SchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/SchematicsBackend.java @@ -19,11 +19,10 @@ package com.sk89q.worldedit.internal.schematic.backends; -import com.sk89q.worldedit.internal.annotation.SchematicPath; import com.sk89q.worldedit.internal.schematic.SchematicsManager; import java.nio.file.Path; -import java.util.List; +import java.util.Set; /** * {@link SchematicsManager} backend interface. @@ -41,9 +40,11 @@ public interface SchematicsBackend { void uninit(); /** - * {@return the list of known schematics} + * Gets the set of known schematic paths. + * + * @return the set of known schematics */ - List getList(); + Set getPaths(); /** * Tells the backend that there are changes it should take into account. diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java index 55a4c06f45..27eb7b55e6 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java @@ -21,6 +21,8 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.util.io.file.FilenameException; import org.apache.logging.log4j.Logger; import java.io.Closeable; @@ -105,14 +107,19 @@ public static Optional create(Path root) throws IOExc } } - private void registerFolderWatchAndScanInitially(Path root) throws IOException { + private void registerFolderWatcher(Path root) throws IOException { WatchKey watchKey = root.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); LOGGER.debug("Watch registered: " + root); watchRootMap.put(watchKey, root); + } + + private void triggerInitialEvents(Path root) throws IOException, FilenameException { + Path schematicRoot = WorldEdit.getInstance().getSchematicsManager().getRoot(); eventConsumer.accept(new DirectoryCreatedEvent(root)); for (Path path : Files.newDirectoryStream(root)) { + path = WorldEdit.getInstance().getSafeOpenFile(null, schematicRoot.toFile(), path.toString(), null).toPath(); if (Files.isDirectory(path)) { - registerFolderWatchAndScanInitially(path); + triggerInitialEvents(path); } else { eventConsumer.accept(new FileCreatedEvent(path)); } @@ -126,13 +133,16 @@ private void registerFolderWatchAndScanInitially(Path root) throws IOException { * @param eventConsumer The lambda that's fired for every file event. */ public void start(Consumer eventConsumer) { + Path schematicRoot = WorldEdit.getInstance().getSchematicsManager().getRoot(); + this.eventConsumer = eventConsumer; watchThread = new Thread(() -> { LOGGER.debug("RecursiveDirectoryWatcher::EventConsumer started"); try { - registerFolderWatchAndScanInitially(root); - } catch (IOException e) { + registerFolderWatcher(root); + triggerInitialEvents(root); + } catch (IOException | FilenameException e) { LOGGER.error(e); } @@ -155,10 +165,13 @@ public void start(Consumer eventConsumer) { path = parentPath.resolve(path); if (kind.equals(StandardWatchEventKinds.ENTRY_CREATE)) { + path = WorldEdit.getInstance().getSafeOpenFile(null, schematicRoot.toFile(), path.toString(), null).toPath(); + if (Files.isDirectory(path)) { // new subfolder created, create watch for it try { - registerFolderWatchAndScanInitially(path); - } catch (IOException e) { + registerFolderWatcher(path); + triggerInitialEvents(path); + } catch (IOException | FilenameException e) { LOGGER.error(e); } } else { // new file created @@ -188,7 +201,8 @@ public void start(Consumer eventConsumer) { } } } - } catch (ClosedWatchServiceException ignored) { } + } catch (ClosedWatchServiceException | FilenameException ignored) { + } LOGGER.debug("RecursiveDirectoryWatcher::EventConsumer exited"); }); watchThread.setName("RecursiveDirectoryWatcher"); From 05b229acbc62b721b593785fae4f6d37bb796f42 Mon Sep 17 00:00:00 2001 From: Madeline Miller Date: Sun, 21 May 2023 21:49:16 +1000 Subject: [PATCH 09/10] Fix symlink check --- .../src/main/java/com/sk89q/worldedit/WorldEdit.java | 4 +++- .../internal/schematic/backends/PollingSchematicsBackend.java | 3 +-- .../worldedit/internal/util/RecursiveDirectoryWatcher.java | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java b/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java index c277f4d6ea..02e9aee961 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java @@ -407,8 +407,10 @@ private File getSafeFileWithExtension(File dir, String filename, String extensio return new File(dir, filename); } + private static final java.util.regex.Pattern SAFE_FILENAME_REGEX = java.util.regex.Pattern.compile("^[A-Za-z0-9_\\- \\./\\\\'\\$@~!%\\^\\*\\(\\)\\[\\]\\+\\{\\},\\?]+\\.[A-Za-z0-9]+$"); + private boolean checkFilename(String filename) { - return filename.matches("^[A-Za-z0-9_\\- \\./\\\\'\\$@~!%\\^\\*\\(\\)\\[\\]\\+\\{\\},\\?]+\\.[A-Za-z0-9]+$"); + return SAFE_FILENAME_REGEX.matcher(filename).matches(); } /** diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java index 05904a6dfd..d8dc827973 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/PollingSchematicsBackend.java @@ -33,7 +33,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; -import java.util.concurrent.locks.ReentrantReadWriteLock; /** * A backend that scans the folder tree, then caches the result for a certain amount of time. @@ -67,7 +66,7 @@ private List scanFolder(Path root) { try (DirectoryStream stream = Files.newDirectoryStream(root)) { for (Path path : stream) { - path = WorldEdit.getInstance().getSafeOpenFile(null, schematicRoot.toFile(), path.toString(), null).toPath(); + path = WorldEdit.getInstance().getSafeOpenFile(null, schematicRoot.toFile(), schematicRoot.relativize(path).toString(), null).toPath(); if (Files.isDirectory(path)) { pathList.addAll(scanFolder(path)); } else { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java index 27eb7b55e6..c50ba193ea 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/RecursiveDirectoryWatcher.java @@ -117,7 +117,7 @@ private void triggerInitialEvents(Path root) throws IOException, FilenameExcepti Path schematicRoot = WorldEdit.getInstance().getSchematicsManager().getRoot(); eventConsumer.accept(new DirectoryCreatedEvent(root)); for (Path path : Files.newDirectoryStream(root)) { - path = WorldEdit.getInstance().getSafeOpenFile(null, schematicRoot.toFile(), path.toString(), null).toPath(); + path = WorldEdit.getInstance().getSafeOpenFile(null, schematicRoot.toFile(), schematicRoot.relativize(path).toString(), null).toPath(); if (Files.isDirectory(path)) { triggerInitialEvents(path); } else { @@ -165,7 +165,7 @@ public void start(Consumer eventConsumer) { path = parentPath.resolve(path); if (kind.equals(StandardWatchEventKinds.ENTRY_CREATE)) { - path = WorldEdit.getInstance().getSafeOpenFile(null, schematicRoot.toFile(), path.toString(), null).toPath(); + path = WorldEdit.getInstance().getSafeOpenFile(null, schematicRoot.toFile(), schematicRoot.relativize(path).toString(), null).toPath(); if (Files.isDirectory(path)) { // new subfolder created, create watch for it try { From ff6393efaf6cc2badcb5fd4ffaac41425e041c95 Mon Sep 17 00:00:00 2001 From: Madeline Miller Date: Sat, 24 Jun 2023 16:48:09 +1000 Subject: [PATCH 10/10] PR notes --- .../com/sk89q/worldedit/extension/platform/Capability.java | 1 - .../schematic/backends/FileWatcherSchematicsBackend.java | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Capability.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Capability.java index 7be3a8b88d..f132bc0473 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Capability.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Capability.java @@ -21,7 +21,6 @@ import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.internal.block.BlockStateIdAccess; -import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.world.block.BlockState; import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.registry.BlockRegistry; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java index 59f2d8c838..b1a3ed1f4e 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/schematic/backends/FileWatcherSchematicsBackend.java @@ -61,17 +61,14 @@ public void init() { try { if (event instanceof RecursiveDirectoryWatcher.FileCreatedEvent) { schematics.add(event.path()); + LOGGER.debug("New Schematic found: " + event.path()); } else if (event instanceof RecursiveDirectoryWatcher.FileDeletedEvent) { schematics.remove(event.path()); + LOGGER.debug("Schematic deleted: " + event.path()); } } finally { lock.writeLock().unlock(); } - if (event instanceof RecursiveDirectoryWatcher.FileCreatedEvent) { - LOGGER.debug("New Schematic found: " + event.path()); - } else if (event instanceof RecursiveDirectoryWatcher.FileDeletedEvent) { - LOGGER.debug("Schematic deleted: " + event.path()); - } }); }