Skip to content

Commit 7b79477

Browse files
seijikunme4502
authored andcommitted
Implement SchematicsManager to cache list of known schematics
1 parent 5f19fb4 commit 7b79477

10 files changed

Lines changed: 734 additions & 26 deletions

File tree

worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import com.sk89q.worldedit.util.io.file.FilenameException;
6666
import com.sk89q.worldedit.util.io.file.FilenameResolutionException;
6767
import com.sk89q.worldedit.util.io.file.InvalidFilenameException;
68+
import com.sk89q.worldedit.util.schematic.SchematicsManager;
6869
import com.sk89q.worldedit.util.task.SimpleSupervisor;
6970
import com.sk89q.worldedit.util.task.Supervisor;
7071
import com.sk89q.worldedit.util.translation.TranslationManager;
@@ -126,6 +127,7 @@ public final class WorldEdit {
126127
EvenMoreExecutors.newBoundedCachedThreadPool(0, 1, 20, "WorldEdit Task Executor - %s"));
127128
private final Supervisor supervisor = new SimpleSupervisor();
128129
private final AssetLoaders assetLoaders = new AssetLoaders(this);
130+
private final SchematicsManager schematicsManager = new SchematicsManager(this);
129131

130132
private final BlockFactory blockFactory = new BlockFactory(this);
131133
private final ItemFactory itemFactory = new ItemFactory(this);
@@ -261,6 +263,15 @@ public AssetLoaders getAssetLoaders() {
261263
return assetLoaders;
262264
}
263265

266+
/**
267+
* Return the Schematics Manager instance.
268+
*
269+
* @return the schematics manager instance
270+
*/
271+
public SchematicsManager getSchematicsManager() {
272+
return schematicsManager;
273+
}
274+
264275
/**
265276
* Gets the path to a file. This method will check to see if the filename
266277
* has valid characters and has an extension. It also prevents directory

worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
import com.sk89q.worldedit.util.io.Closer;
5656
import com.sk89q.worldedit.util.io.file.FilenameException;
5757
import com.sk89q.worldedit.util.io.file.MorePaths;
58+
import com.sk89q.worldedit.util.schematic.Schematic;
59+
import com.sk89q.worldedit.util.schematic.SchematicsManager;
5860
import org.apache.logging.log4j.Logger;
5961
import org.enginehub.piston.annotation.Command;
6062
import org.enginehub.piston.annotation.CommandContainer;
@@ -314,7 +316,6 @@ public void list(Actor actor,
314316
if (oldFirst && newFirst) {
315317
throw new StopExecutionException(TextComponent.of("Cannot sort by oldest and newest."));
316318
}
317-
final String saveDir = worldEdit.getConfiguration().saveDir;
318319
Comparator<Path> pathComparator;
319320
String flag;
320321
if (oldFirst) {
@@ -330,8 +331,10 @@ public void list(Actor actor,
330331
final String pageCommand = actor.isPlayer()
331332
? "//schem list -p %page%" + flag : null;
332333

334+
Comparator<Schematic> schematicComparator = (s0, s1) -> pathComparator.compare(s0.getPath(), s1.getPath());
335+
333336
WorldEditAsyncCommandBuilder.createAndSendMessage(actor,
334-
new SchematicListTask(saveDir, pathComparator, page, pageCommand),
337+
new SchematicListTask(schematicComparator, page, pageCommand),
335338
SubtleFormat.wrap("(Please wait... gathering schematic list.)"));
336339
}
337340

@@ -399,6 +402,7 @@ private static class SchematicSaveTask extends SchematicOutputTask<Void> {
399402
public Void call() throws Exception {
400403
try {
401404
writeToOutputStream(new FileOutputStream(file));
405+
WorldEdit.getInstance().getSchematicsManager().update();
402406
LOGGER.info(actor.getName() + " saved " + file.getCanonicalPath() + (overwrite ? " (overwriting previous file)" : ""));
403407
} catch (IOException e) {
404408
file.delete();
@@ -437,53 +441,37 @@ public Consumer<Actor> call() throws Exception {
437441
}
438442

439443
private static class SchematicListTask implements Callable<Component> {
440-
private final Comparator<Path> pathComparator;
444+
private final Comparator<Schematic> pathComparator;
441445
private final int page;
442-
private final Path rootDir;
443446
private final String pageCommand;
444447

445-
SchematicListTask(String prefix, Comparator<Path> pathComparator, int page, String pageCommand) {
448+
SchematicListTask(Comparator<Schematic> pathComparator, int page, String pageCommand) {
446449
this.pathComparator = pathComparator;
447450
this.page = page;
448-
this.rootDir = WorldEdit.getInstance().getWorkingDirectoryPath(prefix);
449451
this.pageCommand = pageCommand;
450452
}
451453

452454
@Override
453455
public Component call() throws Exception {
454-
Path resolvedRoot = rootDir.toRealPath();
455-
List<Path> fileList = allFiles(resolvedRoot);
456+
SchematicsManager schematicsManager = WorldEdit.getInstance().getSchematicsManager();
457+
List<Schematic> fileList = schematicsManager.getList();
456458

457459
if (fileList.isEmpty()) {
458460
return ErrorFormat.wrap("No schematics found.");
459461
}
460462

461463
fileList.sort(pathComparator);
462464

463-
PaginationBox paginationBox = new SchematicPaginationBox(resolvedRoot, fileList, pageCommand);
465+
PaginationBox paginationBox = new SchematicPaginationBox(schematicsManager.getRoot(), fileList, pageCommand);
464466
return paginationBox.create(page);
465467
}
466468
}
467469

468-
private static List<Path> allFiles(Path root) throws IOException {
469-
List<Path> pathList = new ArrayList<>();
470-
try (DirectoryStream<Path> stream = Files.newDirectoryStream(root)) {
471-
for (Path path : stream) {
472-
if (Files.isDirectory(path)) {
473-
pathList.addAll(allFiles(path));
474-
} else {
475-
pathList.add(path);
476-
}
477-
}
478-
}
479-
return pathList;
480-
}
481-
482470
private static class SchematicPaginationBox extends PaginationBox {
483471
private final Path rootDir;
484-
private final List<Path> files;
472+
private final List<Schematic> files;
485473

486-
SchematicPaginationBox(Path rootDir, List<Path> files, String pageCommand) {
474+
SchematicPaginationBox(Path rootDir, List<Schematic> files, String pageCommand) {
487475
super("Available schematics", pageCommand);
488476
this.rootDir = rootDir;
489477
this.files = files;
@@ -492,7 +480,7 @@ private static class SchematicPaginationBox extends PaginationBox {
492480
@Override
493481
public Component getComponent(int number) {
494482
checkArgument(number < files.size() && number >= 0);
495-
Path file = files.get(number);
483+
Path file = files.get(number).getPath();
496484

497485
String format = ClipboardFormats.getFileExtensionMap()
498486
.get(MoreFiles.getFileExtension(file))

worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Capability.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import com.sk89q.worldedit.WorldEdit;
2323
import com.sk89q.worldedit.internal.block.BlockStateIdAccess;
24+
import com.sk89q.worldedit.internal.util.LogManagerCompat;
2425
import com.sk89q.worldedit.world.block.BlockState;
2526
import com.sk89q.worldedit.world.block.BlockType;
2627
import com.sk89q.worldedit.world.registry.BlockRegistry;
@@ -53,10 +54,12 @@ void uninitialize(PlatformManager platformManager, Platform platform) {
5354
@Override
5455
void initialize(PlatformManager platformManager, Platform platform) {
5556
WorldEdit.getInstance().getAssetLoaders().init();
57+
WorldEdit.getInstance().getSchematicsManager().init();
5658
}
5759

5860
@Override
5961
void uninitialize(PlatformManager platformManager, Platform platform) {
62+
WorldEdit.getInstance().getSchematicsManager().uninit();
6063
WorldEdit.getInstance().getAssetLoaders().uninit();
6164
}
6265
},
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* WorldEdit, a Minecraft world manipulation toolkit
3+
* Copyright (C) sk89q <http://www.sk89q.com>
4+
* Copyright (C) WorldEdit team and contributors
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
package com.sk89q.worldedit.util.io.file;
21+
22+
import com.google.common.collect.BiMap;
23+
import com.google.common.collect.HashBiMap;
24+
import com.sk89q.worldedit.internal.util.LogManagerCompat;
25+
import org.apache.logging.log4j.Logger;
26+
27+
import java.io.IOException;
28+
import java.nio.file.ClosedWatchServiceException;
29+
import java.nio.file.Files;
30+
import java.nio.file.Path;
31+
import java.nio.file.StandardWatchEventKinds;
32+
import java.nio.file.WatchEvent;
33+
import java.nio.file.WatchKey;
34+
import java.nio.file.WatchService;
35+
import java.util.function.Consumer;
36+
37+
/**
38+
* Helper class that allows to recursively monitor a directory for changed (created / deleted) files / folders.
39+
*
40+
* @warning File- and Folder events might be sent multiple times. Users of this class need to employ their own
41+
* deduplication!
42+
*/
43+
public class RecursiveDirectoryWatcher {
44+
45+
/**
46+
* Base-Class for all DirEntry change events.
47+
*/
48+
public static class DirEntryChangeEvent {
49+
private Path path;
50+
51+
public DirEntryChangeEvent(Path path) {
52+
this.path = path;
53+
}
54+
55+
public Path getPath() {
56+
return path;
57+
}
58+
}
59+
60+
/**
61+
* Event signaling the creation of a new file.
62+
*/
63+
public static class FileCreatedEvent extends DirEntryChangeEvent {
64+
public FileCreatedEvent(Path path) {
65+
super(path);
66+
}
67+
}
68+
69+
/**
70+
* Event signaling the deletion of a file.
71+
*/
72+
public static class FileDeletedEvent extends DirEntryChangeEvent {
73+
public FileDeletedEvent(Path path) {
74+
super(path);
75+
}
76+
}
77+
78+
/**
79+
* Event signaling the creation of a new directory.
80+
*/
81+
public static class DirectoryCreatedEvent extends DirEntryChangeEvent {
82+
public DirectoryCreatedEvent(Path path) {
83+
super(path);
84+
}
85+
}
86+
87+
/**
88+
* Event signaling the deletion of a directory.
89+
*/
90+
public static class DirectoryDeletedEvent extends DirEntryChangeEvent {
91+
public DirectoryDeletedEvent(Path path) {
92+
super(path);
93+
}
94+
}
95+
96+
97+
private static final Logger LOGGER = LogManagerCompat.getLogger();
98+
99+
private final Path root;
100+
private final WatchService watchService;
101+
private Thread watchThread;
102+
private Consumer<DirEntryChangeEvent> eventConsumer;
103+
private BiMap<WatchKey, Path> watchRootMap = HashBiMap.create();
104+
105+
private RecursiveDirectoryWatcher(Path root, WatchService watchService) {
106+
this.root = root;
107+
this.watchService = watchService;
108+
}
109+
110+
/**
111+
* Create a new recursive directory watcher for the given root folder.
112+
* You have to call @see start() before the instance starts monitoring.
113+
*
114+
* @param root Folder to watch for changed files recursively.
115+
* @return A new RecursiveDirectoryWatcher instance, monitoring the given root folder.
116+
* @throws IOException If creating the watcher failed, e.g. due to root not existing.
117+
*/
118+
public static RecursiveDirectoryWatcher create(Path root) throws IOException {
119+
WatchService watchService = root.getFileSystem().newWatchService();
120+
return new RecursiveDirectoryWatcher(root, watchService);
121+
}
122+
123+
private void registerFolderWatchAndScanInitially(Path root) throws IOException {
124+
WatchKey watchKey = root.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
125+
LOGGER.debug("Watch registered: " + root);
126+
watchRootMap.put(watchKey, root);
127+
eventConsumer.accept(new DirectoryCreatedEvent(root));
128+
for (Path path : Files.newDirectoryStream(root)) {
129+
if (Files.isDirectory(path)) {
130+
registerFolderWatchAndScanInitially(path);
131+
} else {
132+
eventConsumer.accept(new FileCreatedEvent(path));
133+
}
134+
}
135+
}
136+
137+
/**
138+
* Make this RecursiveDirectoryWatcher instance start monitoring the root folder it was created on.
139+
* When this is called, RecursiveDirectoryWatcher will send initial notifications for the entire
140+
* file structure in the configured root.
141+
* @param eventConsumer The lambda that's fired for every file event.
142+
*/
143+
public void start(Consumer<DirEntryChangeEvent> eventConsumer) {
144+
this.eventConsumer = eventConsumer;
145+
watchThread = new Thread(() -> {
146+
LOGGER.debug("RecursiveDirectoryWatcher::EventConsumer started");
147+
148+
try {
149+
registerFolderWatchAndScanInitially(root);
150+
} catch (IOException e) { e.printStackTrace(); }
151+
152+
try {
153+
WatchKey watchKey;
154+
while (true) {
155+
try {
156+
watchKey = watchService.take();
157+
} catch (InterruptedException e) { break; }
158+
159+
for (WatchEvent<?> event : watchKey.pollEvents()) {
160+
WatchEvent.Kind<?> kind = event.kind();
161+
if (kind.equals(StandardWatchEventKinds.OVERFLOW)) {
162+
LOGGER.warn("RecursiveDirectoryWatcher Seems like we can't keep up with updates");
163+
continue;
164+
}
165+
// make sure to work with an absolute path
166+
Path path = (Path) event.context();
167+
Path parentPath = watchRootMap.get(watchKey);
168+
path = parentPath.resolve(path);
169+
170+
if (kind.equals(StandardWatchEventKinds.ENTRY_CREATE)) {
171+
if (Files.isDirectory(path)) { // new subfolder created, create watch for it
172+
try {
173+
registerFolderWatchAndScanInitially(path);
174+
} catch (IOException e) { e.printStackTrace(); }
175+
} else { // new file created
176+
eventConsumer.accept(new FileCreatedEvent(path));
177+
}
178+
} else if (kind.equals(StandardWatchEventKinds.ENTRY_DELETE)) {
179+
// When we are notified about a deleted entry, we can't simply ask the filesystem
180+
// whether the entry is a file or a folder. But we have our watchRootMap, that stores
181+
// one WatchKey per (sub)folder, so we can just ask it.
182+
if (watchRootMap.containsValue(path)) { // was a folder
183+
LOGGER.debug("Watch unregistered: " + path);
184+
WatchKey obsoleteSubfolderWatchKey = watchRootMap.inverse().get(path);
185+
// stop listening to changes from deleted dir
186+
obsoleteSubfolderWatchKey.cancel();
187+
watchRootMap.remove(obsoleteSubfolderWatchKey);
188+
eventConsumer.accept(new DirectoryDeletedEvent(path));
189+
} else { // was a file
190+
eventConsumer.accept(new FileDeletedEvent(path));
191+
}
192+
}
193+
}
194+
195+
if (!watchKey.reset()) {
196+
watchRootMap.remove(watchKey);
197+
if (watchRootMap.isEmpty()) {
198+
break; // nothing left to watch
199+
}
200+
}
201+
}
202+
} catch (ClosedWatchServiceException ignored) { }
203+
LOGGER.debug("RecursiveDirectoryWatcher::EventConsumer exited");
204+
});
205+
watchThread.setName("RecursiveDirectoryWatcher");
206+
watchThread.start();
207+
}
208+
209+
/**
210+
* Stop this RecursiveDirectoryWatcher instance and wait for it to be completely shut down.
211+
* @warning RecursiveDirectoryWatcher is not reusable!
212+
*/
213+
public void stop() {
214+
try {
215+
watchService.close();
216+
} catch (IOException e) { e.printStackTrace(); }
217+
if (watchThread != null) {
218+
try {
219+
watchThread.join();
220+
} catch (InterruptedException e) { e.printStackTrace(); }
221+
watchThread = null;
222+
}
223+
eventConsumer = null;
224+
}
225+
226+
}

0 commit comments

Comments
 (0)