Skip to content

Commit 64aae80

Browse files
tastybentoclaude
andcommitted
Extract AbstractPurgeCommand to remove duplication between purge subcommands
AdminPurgeCommand and AdminPurgeDeletedCommand each carried their own inPurge/toBeConfirmed/lastScan state machine and near-identical canExecute / scan-and-prompt / deleteEverything bodies (~75 lines apiece, diverging only in scan source, log prefix, confirm message and an optional chunk-evict step). Pull the shared scaffolding into a package-private AbstractPurgeCommand, with subclasses supplying: - logPrefix() — log line prefix - successMessageKey() — locale key sent on a successful delete - sendConfirmPrompt() — main-thread prompt(s) before user types confirm - beforeDelete(scan) — optional pre-delete hook (used by /purge deleted to evict in-memory chunks before the async file delete) - logScanContents(islands, scan) — optional scan-time logging Behaviour is preserved: same locale keys, same log prefixes, same async threading. The "after a non-empty scan" tail on the failed-delete log line in /purge deleted is dropped — it was always true (we'd already returned on empty) and made the message harder to share. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent fcdca35 commit 64aae80

File tree

3 files changed

+176
-150
lines changed

3 files changed

+176
-150
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package world.bentobox.bentobox.api.commands.admin.purge;
2+
3+
import java.util.Optional;
4+
import java.util.Set;
5+
import java.util.function.Supplier;
6+
import java.util.stream.Collectors;
7+
8+
import org.bukkit.Bukkit;
9+
import org.bukkit.World;
10+
11+
import world.bentobox.bentobox.api.commands.CompositeCommand;
12+
import world.bentobox.bentobox.api.localization.TextVariables;
13+
import world.bentobox.bentobox.api.user.User;
14+
import world.bentobox.bentobox.database.objects.Island;
15+
import world.bentobox.bentobox.managers.PurgeRegionsService.PurgeScanResult;
16+
17+
/**
18+
* Shared scaffolding for purge subcommands that scan for region files and
19+
* (on confirmation) reap them via {@link world.bentobox.bentobox.managers.PurgeRegionsService}.
20+
*
21+
* <p>Subclasses supply the scan source (age-filtered or deletable-only),
22+
* confirmation prompt, success message and any pre-delete side-effects via
23+
* the abstract / overridable hooks.
24+
*/
25+
abstract class AbstractPurgeCommand extends CompositeCommand {
26+
27+
protected static final String NONE_FOUND = "commands.admin.purge.none-found";
28+
29+
protected volatile boolean inPurge;
30+
protected boolean toBeConfirmed;
31+
protected User user;
32+
protected PurgeScanResult lastScan;
33+
34+
protected AbstractPurgeCommand(CompositeCommand parent, String label) {
35+
super(parent, label);
36+
}
37+
38+
@Override
39+
public boolean canExecute(User user, String label, java.util.List<String> args) {
40+
if (inPurge) {
41+
user.sendMessage("commands.admin.purge.purge-in-progress", TextVariables.LABEL, this.getTopLabel());
42+
return false;
43+
}
44+
return true;
45+
}
46+
47+
/**
48+
* Saves all worlds, runs the scan on an async thread, stores the result
49+
* in {@link #lastScan} and prompts for confirmation if non-empty.
50+
*/
51+
protected final void runScanAndPrompt(Supplier<PurgeScanResult> scanFn) {
52+
user.sendMessage("commands.admin.purge.scanning");
53+
getPlugin().log(logPrefix() + ": saving all worlds before scanning...");
54+
Bukkit.getWorlds().forEach(World::save);
55+
getPlugin().log(logPrefix() + ": world save complete");
56+
57+
inPurge = true;
58+
Bukkit.getScheduler().runTaskAsynchronously(getPlugin(), () -> {
59+
try {
60+
lastScan = scanFn.get();
61+
displayResultsAndPrompt(lastScan);
62+
} finally {
63+
inPurge = false;
64+
}
65+
});
66+
}
67+
68+
/**
69+
* Confirm path: save worlds, run any pre-delete hook, then dispatch the
70+
* region-file deletion asynchronously.
71+
*/
72+
protected final boolean deleteEverything() {
73+
if (lastScan == null || lastScan.isEmpty()) {
74+
user.sendMessage(NONE_FOUND);
75+
return false;
76+
}
77+
PurgeScanResult scan = lastScan;
78+
lastScan = null;
79+
toBeConfirmed = false;
80+
getPlugin().log(logPrefix() + ": saving all worlds before deleting region files...");
81+
Bukkit.getWorlds().forEach(World::save);
82+
beforeDelete(scan);
83+
getPlugin().log(logPrefix() + ": world save complete, dispatching deletion");
84+
Bukkit.getScheduler().runTaskAsynchronously(getPlugin(), () -> {
85+
boolean ok = getPlugin().getPurgeRegionsService().delete(scan);
86+
Bukkit.getScheduler().runTask(getPlugin(), () -> {
87+
if (ok) {
88+
user.sendMessage(successMessageKey());
89+
} else {
90+
getPlugin().log(logPrefix() + ": failed to delete one or more region files");
91+
user.sendMessage("commands.admin.purge.failed");
92+
}
93+
});
94+
});
95+
return true;
96+
}
97+
98+
private void displayResultsAndPrompt(PurgeScanResult scan) {
99+
Set<Island> uniqueIslands = scan.deletableRegions().values().stream()
100+
.flatMap(Set::stream)
101+
.map(getPlugin().getIslands()::getIslandById)
102+
.flatMap(Optional::stream)
103+
.collect(Collectors.toSet());
104+
105+
logScanContents(uniqueIslands, scan);
106+
107+
if (scan.isEmpty()) {
108+
Bukkit.getScheduler().runTask(getPlugin(), () -> user.sendMessage(NONE_FOUND));
109+
} else {
110+
Bukkit.getScheduler().runTask(getPlugin(), () -> {
111+
user.sendMessage("commands.admin.purge.purgable-islands",
112+
TextVariables.NUMBER, String.valueOf(uniqueIslands.size()));
113+
sendConfirmPrompt();
114+
toBeConfirmed = true;
115+
});
116+
}
117+
}
118+
119+
/** Log prefix used across the scan and delete phases (e.g. {@code "Purge"}). */
120+
protected abstract String logPrefix();
121+
122+
/** Locale key sent to the user when delete completes successfully. */
123+
protected abstract String successMessageKey();
124+
125+
/** Send any subclass-specific confirmation prompt(s). Runs on the main thread. */
126+
protected abstract void sendConfirmPrompt();
127+
128+
/** Hook invoked on the main thread immediately before the async delete. Default: no-op. */
129+
protected void beforeDelete(PurgeScanResult scan) {
130+
// no-op
131+
}
132+
133+
/** Hook invoked off-thread to log scan contents before the prompt. Default: no-op. */
134+
protected void logScanContents(Set<Island> uniqueIslands, PurgeScanResult scan) {
135+
// no-op
136+
}
137+
}

src/main/java/world/bentobox/bentobox/api/commands/admin/purge/AdminPurgeCommand.java

Lines changed: 18 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@
44
import java.time.ZoneId;
55
import java.time.format.DateTimeFormatter;
66
import java.util.List;
7-
import java.util.Optional;
87
import java.util.Set;
9-
import java.util.stream.Collectors;
10-
11-
import org.bukkit.Bukkit;
12-
import org.bukkit.World;
138

149
import world.bentobox.bentobox.api.commands.CompositeCommand;
1510
import world.bentobox.bentobox.api.localization.TextVariables;
@@ -33,17 +28,11 @@
3328
*
3429
* <p>Heavy lifting is delegated to {@link PurgeRegionsService}.
3530
*/
36-
public class AdminPurgeCommand extends CompositeCommand {
31+
public class AdminPurgeCommand extends AbstractPurgeCommand {
3732

38-
private static final String NONE_FOUND = "commands.admin.purge.none-found";
3933
private static final String IN_WORLD = " in world ";
4034
private static final String WILL_BE_DELETED = " will be deleted";
4135

42-
private volatile boolean inPurge;
43-
private boolean toBeConfirmed;
44-
private User user;
45-
private PurgeScanResult lastScan;
46-
4736
public AdminPurgeCommand(CompositeCommand parent) {
4837
super(parent, "purge");
4938
}
@@ -62,8 +51,7 @@ public void setup() {
6251

6352
@Override
6453
public boolean canExecute(User user, String label, List<String> args) {
65-
if (inPurge) {
66-
user.sendMessage("commands.admin.purge.purge-in-progress", TextVariables.LABEL, this.getTopLabel());
54+
if (!super.canExecute(user, label, args)) {
6755
return false;
6856
}
6957
if (args.isEmpty()) {
@@ -93,75 +81,33 @@ public boolean execute(User user, String label, List<String> args) {
9381
return false;
9482
}
9583

96-
user.sendMessage("commands.admin.purge.scanning");
97-
getPlugin().log("Purge: saving all worlds before scanning region files...");
98-
Bukkit.getWorlds().forEach(World::save);
99-
getPlugin().log("Purge: world save complete");
100-
101-
inPurge = true;
10284
final int finalDays = days;
103-
Bukkit.getScheduler().runTaskAsynchronously(getPlugin(), () -> {
104-
try {
105-
PurgeRegionsService service = getPlugin().getPurgeRegionsService();
106-
lastScan = service.scan(getWorld(), finalDays);
107-
displayResultsAndPrompt(lastScan);
108-
} finally {
109-
inPurge = false;
110-
}
111-
});
85+
runScanAndPrompt(() -> getPlugin().getPurgeRegionsService().scan(getWorld(), finalDays));
11286
return true;
11387
}
11488

115-
private boolean deleteEverything() {
116-
if (lastScan == null || lastScan.isEmpty()) {
117-
user.sendMessage(NONE_FOUND);
118-
return false;
119-
}
120-
PurgeScanResult scan = lastScan;
121-
lastScan = null;
122-
toBeConfirmed = false;
123-
getPlugin().log("Purge: saving all worlds before deleting region files...");
124-
Bukkit.getWorlds().forEach(World::save);
125-
getPlugin().log("Purge: world save complete, dispatching deletion");
126-
Bukkit.getScheduler().runTaskAsynchronously(getPlugin(), () -> {
127-
boolean ok = getPlugin().getPurgeRegionsService().delete(scan);
128-
Bukkit.getScheduler().runTask(getPlugin(), () -> {
129-
if (ok) {
130-
user.sendMessage("general.success");
131-
} else {
132-
getPlugin().log("Purge: failed to delete one or more region files");
133-
user.sendMessage("commands.admin.purge.failed");
134-
}
135-
});
136-
});
137-
return true;
89+
@Override
90+
protected String logPrefix() {
91+
return "Purge";
13892
}
13993

140-
private void displayResultsAndPrompt(PurgeScanResult scan) {
141-
Set<Island> uniqueIslands = scan.deletableRegions().values().stream()
142-
.flatMap(Set::stream)
143-
.map(getPlugin().getIslands()::getIslandById)
144-
.flatMap(Optional::stream)
145-
.collect(Collectors.toSet());
94+
@Override
95+
protected String successMessageKey() {
96+
return "general.success";
97+
}
14698

147-
uniqueIslands.forEach(this::displayIsland);
99+
@Override
100+
protected void sendConfirmPrompt() {
101+
user.sendMessage("commands.admin.purge.confirm", TextVariables.LABEL, this.getTopLabel());
102+
user.sendMessage("general.beta");
103+
}
148104

105+
@Override
106+
protected void logScanContents(Set<Island> uniqueIslands, PurgeScanResult scan) {
107+
uniqueIslands.forEach(this::displayIsland);
149108
scan.deletableRegions().entrySet().stream()
150109
.filter(e -> e.getValue().isEmpty())
151110
.forEach(e -> displayEmptyRegion(e.getKey()));
152-
153-
if (scan.isEmpty()) {
154-
Bukkit.getScheduler().runTask(getPlugin(), () -> user.sendMessage(NONE_FOUND));
155-
} else {
156-
Bukkit.getScheduler().runTask(getPlugin(), () -> {
157-
user.sendMessage("commands.admin.purge.purgable-islands",
158-
TextVariables.NUMBER, String.valueOf(uniqueIslands.size()));
159-
user.sendMessage("commands.admin.purge.confirm",
160-
TextVariables.LABEL, this.getTopLabel());
161-
user.sendMessage("general.beta");
162-
toBeConfirmed = true;
163-
});
164-
}
165111
}
166112

167113
private void displayIsland(Island island) {

0 commit comments

Comments
 (0)