1+ package com .example .disenchanter .logging ;
2+
3+ import com .example .disenchanter .DisenchanterPlugin ;
4+ import org .bukkit .Bukkit ;
5+
6+ import java .io .BufferedWriter ;
7+ import java .io .File ;
8+ import java .io .FileWriter ;
9+ import java .io .IOException ;
10+ import java .nio .charset .StandardCharsets ;
11+ import java .time .LocalDate ;
12+ import java .time .LocalDateTime ;
13+ import java .time .format .DateTimeFormatter ;
14+ import java .util .concurrent .ConcurrentLinkedQueue ;
15+ import java .util .concurrent .ExecutorService ;
16+ import java .util .concurrent .Executors ;
17+ import java .util .concurrent .TimeUnit ;
18+
19+ /**
20+ * Handles operation logging to console and file.
21+ * <p>
22+ * Queue-based logging with asynchronous flush to daily log files.
23+ * Uses config's {@code logging.format} string with placeholder replacement:
24+ * <ul>
25+ * <li>{@code {time}} — timestamp</li>
26+ * <li>{@code {player}} — player name</li>
27+ * <li>{@code {uuid}} — player UUID</li>
28+ * <li>{@code {item}} — item type name</li>
29+ * <li>{@code {enchant}} — enchantment key</li>
30+ * <li>{@code {level}} — enchantment level</li>
31+ * <li>{@code {mode}} — removal mode (individual/bulk)</li>
32+ * <li>{@code {cost}} — cost description</li>
33+ * <li>{@code {result}} — result status (success/skip/deny)</li>
34+ * </ul>
35+ */
36+ public class DisenchantLogger {
37+
38+ private static final DateTimeFormatter TIME_FORMATTER =
39+ DateTimeFormatter .ofPattern ("yyyy-MM-dd HH:mm:ss" );
40+
41+ private final DisenchanterPlugin plugin ;
42+ private final ConcurrentLinkedQueue <String > logQueue = new ConcurrentLinkedQueue <>();
43+ private final ExecutorService fileExecutor = Executors .newSingleThreadExecutor (r -> {
44+ Thread t = new Thread (r , "Disenchanter-LogWriter" );
45+ t .setDaemon (true );
46+ return t ;
47+ });
48+
49+ private File logDir ;
50+ private volatile boolean shutdown = false ;
51+
52+ public DisenchantLogger (DisenchanterPlugin plugin ) {
53+ this .plugin = plugin ;
54+ }
55+
56+ /** Initialize logger — create log directory if file logging is enabled. */
57+ public void init () {
58+ boolean fileLogging = plugin .getConfigManager ().isFileLogging ();
59+ if (fileLogging ) {
60+ logDir = new File (plugin .getDataFolder (), "logs" );
61+ if (!logDir .exists ()) {
62+ logDir .mkdirs ();
63+ }
64+ }
65+ }
66+
67+ /**
68+ * Log a disenchant operation.
69+ *
70+ * @param playerName player name
71+ * @param playerUuid player UUID
72+ * @param enchant enchantment display key (e.g. "minecraft:sharpness")
73+ * @param level enchantment level
74+ * @param itemName item type name
75+ * @param cost cost description
76+ * @param mode removal mode label ("individual" or "bulk")
77+ * @param result result label ("success", "skip", "deny")
78+ */
79+ public void log (String playerName , String playerUuid , String enchant , int level ,
80+ String itemName , String cost , String mode , String result ) {
81+ if (shutdown ) return ;
82+
83+ String time = LocalDateTime .now ().format (TIME_FORMATTER );
84+ String format = plugin .getConfigManager ().getLogFormat ();
85+
86+ // Apply placeholder replacement
87+ String message = format
88+ .replace ("{time}" , time )
89+ .replace ("{player}" , playerName )
90+ .replace ("{uuid}" , playerUuid )
91+ .replace ("{enchant}" , enchant )
92+ .replace ("{level}" , String .valueOf (level ))
93+ .replace ("{item}" , itemName )
94+ .replace ("{cost}" , cost )
95+ .replace ("{mode}" , mode )
96+ .replace ("{result}" , result );
97+
98+ // Console logging — on main thread is fine
99+ if (plugin .getConfigManager ().isConsoleLogging ()) {
100+ plugin .getLogger ().info ("[Disenchant] " + message );
101+ }
102+
103+ // File logging — queue and flush asynchronously
104+ if (plugin .getConfigManager ().isFileLogging () && logDir != null ) {
105+ logQueue .add (message );
106+ scheduleFlush ();
107+ }
108+ }
109+
110+ /** Schedule an async flush of queued log entries using a safe Java executor. */
111+ private void scheduleFlush () {
112+ if (shutdown ) return ;
113+ fileExecutor .submit (this ::flushQueue );
114+ }
115+
116+ /** Flush queued log entries to disk on the executor thread. */
117+ private void flushQueue () {
118+ if (logQueue .isEmpty ()) return ;
119+
120+ String today = LocalDate .now ().format (DateTimeFormatter .ISO_LOCAL_DATE );
121+ File logFile = new File (logDir , "disenchant-" + today + ".log" );
122+
123+ try (BufferedWriter writer = new BufferedWriter (
124+ new FileWriter (logFile , StandardCharsets .UTF_8 , true ))) {
125+ String entry ;
126+ while ((entry = logQueue .poll ()) != null ) {
127+ writer .write (entry );
128+ writer .newLine ();
129+ }
130+ } catch (IOException e ) {
131+ plugin .getLogger ().warning ("Failed to write log: " + e .getMessage ());
132+ }
133+ }
134+
135+ /**
136+ * Shutdown the logger — flush remaining entries synchronously and
137+ * shut down the async executor. Blocks until all pending writes complete.
138+ */
139+ public void shutdown () {
140+ shutdown = true ;
141+ // Flush remaining queue synchronously
142+ flushQueue ();
143+ // Shut down the async executor
144+ fileExecutor .shutdown ();
145+ try {
146+ if (!fileExecutor .awaitTermination (5 , TimeUnit .SECONDS )) {
147+ fileExecutor .shutdownNow ();
148+ plugin .getLogger ().warning ("Log file executor did not terminate in time." );
149+ }
150+ } catch (InterruptedException e ) {
151+ fileExecutor .shutdownNow ();
152+ Thread .currentThread ().interrupt ();
153+ }
154+ }
155+ }
0 commit comments