diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/LoggerID.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/LoggerID.java
new file mode 100644
index 00000000..c10ef992
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/LoggerID.java
@@ -0,0 +1,25 @@
+package fr.neatmonster.nocheatplus.logging;
+
+/**
+ * Restrictions for registration:
+ *
Unique instances.
+ * Unique names.
+ * Custom registrations can not start with the default prefix (see AbstractLogManager).
+ *
+ * @author dev1mc
+ *
+ */
+public class LoggerID {
+
+ public final String name;
+
+ public LoggerID(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "";
+ }
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/StreamID.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/StreamID.java
new file mode 100644
index 00000000..130a76c4
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/StreamID.java
@@ -0,0 +1,25 @@
+package fr.neatmonster.nocheatplus.logging;
+
+/**
+ * Restrictions for registration:
+ * Unique instances.
+ * Unique names.
+ * Custom registrations can not start with the default prefix (see AbstractLogManager).
+ *
+ * @author dev1mc
+ *
+ */
+public class StreamID {
+
+ public final String name;
+
+ public StreamID(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return "";
+ }
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/Streams.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/Streams.java
new file mode 100644
index 00000000..645a3d41
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/Streams.java
@@ -0,0 +1,30 @@
+package fr.neatmonster.nocheatplus.logging;
+
+/**
+ * Default StreamIDs.
+ * @author dev1mc
+ *
+ */
+public class Streams {
+
+ // Maybe temporary: StremID instances for default usage (custom registrations can not start with "default.").
+ /**
+ * Default prefix, should not be used with custom registration (LoggerID, StreamID).
+ */
+ public static final String defaultPrefix = "default.";
+
+ // More or less raw default streams.
+
+ /** For initialization and shutdown, not more than primary thread only is guaranteed here. Always available (fall-back). */
+ public static final StreamID INIT = new StreamID(defaultPrefix + "init");
+ /** Might not allow asynchronous access. */
+ public static final StreamID SERVER_LOGGER = new StreamID(defaultPrefix + "logger.server");
+ /** Might not allow asynchronous access. */
+ public static final StreamID PLUGIN_LOGGER = new StreamID(defaultPrefix + "logger.plugin");
+ public static final StreamID NOTIFY_INGAME = new StreamID(defaultPrefix + "chat.notify");
+ public static final StreamID DEFAULT_FILE = new StreamID(defaultPrefix + "file");
+ public static final StreamID TRACE_FILE = new StreamID(defaultPrefix + "file.trace");
+
+ // TODO: More abstract streams (init, trace, violations?), have loggers/files be LoggerID ?
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogManager.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogManager.java
new file mode 100644
index 00000000..7c345ffc
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogManager.java
@@ -0,0 +1,487 @@
+package fr.neatmonster.nocheatplus.logging.details;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import fr.neatmonster.nocheatplus.logging.LoggerID;
+import fr.neatmonster.nocheatplus.logging.StreamID;
+import fr.neatmonster.nocheatplus.utilities.StringUtil;
+
+/**
+ * Central access point for logging. Abstract class providing basic registration functionality.
+ * @author dev1mc
+ *
+ */
+public abstract class AbstractLogManager {
+
+ // TODO: Visibility of methods.
+ // TODO: Add option for per stream prefixes.
+ // TODO: Concept for adding in the time at the point of call/scheduling.
+
+ // TODO: Add a void stream (no loggers, allow logging unknown ids to void).
+ // TODO: Allow route log with non existing stream to init or void (flag).
+ // TODO: Attaching loggers with different options to different streams is necessary (e.g. normal -> async logging, but any thread with init).
+ // TODO: Re-register with other options: Add methods for LoggerID + StreamID + options.
+ // TODO: Hierarchical LogNode relations, to ensure other log nodes with the same logger are removed too [necessary to allow removing individual loggers].
+
+ // TODO: Temporary streams, e.g. for players, unregistering with command and/or logout.
+ // TODO: Mechanics of removing temporary streams: 1. remove
+
+ // TODO: Consider generalizing the (internal) implementation right away (sub registry by content class).
+ // TODO: Consider adding a global cache (good for re-mapping, contra: reload is not intended to happen on a regular basis).
+
+ private final LogNodeDispatcher dispatcher;
+ private final String defaultPrefix;
+
+ /**
+ * Fast streamID access map (runtime). Copy on write with registryLock.
+ */
+ private Map> idStreamMap = new IdentityHashMap>();
+
+ /**
+ * Map name to Stream. Copy on write with registryLock.
+ */
+ private Map> nameStreamMap = new HashMap>();
+
+ /**
+ * Lower-case name to StreamID.
+ */
+ private Map nameStreamIDMap = new HashMap();
+
+ /**
+ * LogNode registry by LoggerID. Copy on write with registryLock.
+ */
+ private Map> idNodeMap = new IdentityHashMap>();
+
+ /**
+ * LogNode registry by lower-case name. Copy on write with registryLock.
+ */
+ private Map> nameNodeMap = new HashMap>();
+
+ /**
+ * Lower-case name to LoggerID.
+ */
+ private Map nameLoggerIDMap = new HashMap();
+
+ /** Registry changes have to be done under this lock (copy on write) */
+ protected final Object registryCOWLock = new Object();
+ // TODO: Future: Only an init string stream or (later) always "the init stream" for all content types.
+ private final StreamID initStreamID;
+ private final StreamID voidStreamID = new StreamID("void");
+ /**
+ * Fall-back StreamID, for the case of logging to a non-existing StreamID.
+ * By default set to void, but can be altered. Set to null to have calls
+ * fail.
+ */
+ private StreamID fallBackStreamID = voidStreamID;
+
+ /**
+ * Wrapping logging to the init stream.
+ */
+ protected final ContentLogger initLogger = new ContentLogger() {
+ @Override
+ public void log(final Level level, final String content) {
+ AbstractLogManager.this.log(getInitStreamID(), level, content);
+ }
+ };
+
+ /**
+ *
+ * @param dispatcher
+ * @param defaultPrefix
+ * @param initStreamID This id is stored, the stream is created, but no loggers will be attached to it within this constructor.
+ */
+ public AbstractLogManager(LogNodeDispatcher dispatcher, String defaultPrefix, StreamID initStreamID) {
+ this.dispatcher = dispatcher;
+ this.defaultPrefix = defaultPrefix;
+ this.initStreamID = initStreamID;
+ createInitStream();
+ registerInitLogger();
+ dispatcher.setInitLogger(initLogger);
+ }
+
+ /**
+ * Create stream if it does not exist.
+ */
+ protected void createInitStream() {
+ synchronized (registryCOWLock) {
+ if (!hasStream(initStreamID)) {
+ createStringStream(initStreamID);
+ }
+ }
+ }
+
+ /**
+ * Create the minimal init logger(s). Synchronize over registryCOWLock. It's preferable not to duplicate loggers. Prefer LoggerID("init...").
+ */
+ protected abstract void registerInitLogger();
+
+ protected LogNodeDispatcher getLogNodeDispatcher() {
+ return dispatcher;
+ }
+
+ /**
+ * Don't use this prefix for custom registrations with StreamID and LoggerID.
+ * @return
+ */
+ public String getDefaultPrefix() {
+ return defaultPrefix;
+ }
+
+ /**
+ * This should be a fail-safe direct String-logger, that has the highest probability of working
+ * within the default context and rather skips messages instead of failing or scheduling tasks,
+ * typically the main application primary thread.
+ *
+ * @return
+ */
+ public StreamID getInitStreamID() {
+ return initStreamID;
+ }
+
+ /**
+ * A stream that skips all messages. It's not registered officially.
+ * @return
+ */
+ public StreamID getVoidStreamID() {
+ return voidStreamID;
+ }
+
+ /**
+ * Case-insensitive lookup.
+ * @param name
+ * @return Returns the registered StreamID or null, if not present.
+ */
+ public StreamID getStreamID(String name) {
+ return nameStreamIDMap.get(name.toLowerCase());
+ }
+
+ /**
+ * Case-insensitive lookup.
+ * @param name
+ * @return Returns the registered StreamID or null, if not present.
+ */
+ public LoggerID getLoggerID(String name) {
+ return nameLoggerIDMap.get(name.toLowerCase());
+ }
+
+ public void debug(final StreamID streamID, final String message) {
+ log(streamID, Level.FINE, message); // TODO: Not sure what happens with FINE and provided Logger instances.
+ }
+
+ public void info(final StreamID streamID, final String message) {
+ log(streamID, Level.INFO, message);
+ }
+
+ public void warning(final StreamID streamID, final String message) {
+ log(streamID, Level.WARNING, message);
+ }
+ public void severe(final StreamID streamID, final String message) {
+ log(streamID, Level.SEVERE, message);
+ }
+
+ public void log(final StreamID streamID, final Level level, final String message) {
+ if (streamID != voidStreamID) {
+ final ContentStream stream = idStreamMap.get(streamID);
+ if (stream != null) {
+ stream.log(level, message);
+ } else {
+ handleFallBack(streamID, level, message);
+ }
+ }
+ }
+
+ private void handleFallBack(final StreamID streamID, final Level level, final String message) {
+ if (fallBackStreamID != null && streamID != fallBackStreamID) {
+ log(fallBackStreamID, level, message);
+ } else {
+ throw new RuntimeException("Stream not registered: " + streamID);
+ }
+ }
+
+ public void debug(final StreamID streamID, final Throwable t) {
+ log(streamID, Level.FINE, t); // TODO: Not sure what happens with FINE and provided Logger instances.
+ }
+
+ public void info(final StreamID streamID, final Throwable t) {
+ log(streamID, Level.INFO, t);
+ }
+
+ public void warning(final StreamID streamID, final Throwable t) {
+ log(streamID, Level.WARNING, t);
+ }
+
+ public void severe(final StreamID streamID, final Throwable t) {
+ log(streamID, Level.SEVERE, t);
+ }
+
+ public void log(final StreamID streamID, final Level level, final Throwable t) {
+ // Not sure adding streams for Throwable would be better.
+ log(streamID, level, StringUtil.throwableToString(t));
+ }
+
+ /**
+ * A newly created id can be used here. For logging use existing ids always.
+ * @param streamID
+ * @return
+ */
+ public boolean hasStream(final StreamID streamID) {
+ return this.idStreamMap.containsKey(streamID) || this.nameStreamMap.containsKey(streamID.name.toLowerCase());
+ }
+
+ public boolean hasStream(String name) {
+ return getStreamID(name) != null;
+ }
+
+ /**
+ * Call under lock.
+ * @param streamID
+ */
+ private void testRegisterStream(final StreamID streamID) {
+ if (streamID == null) {
+ throw new NullPointerException("StreamID must not be null.");
+ }
+ else if (streamID.name == null) {
+ throw new NullPointerException("StreamID.name must not be null.");
+ }
+ else if (streamID.name.equalsIgnoreCase(voidStreamID.name)) {
+ throw new RuntimeException("Can not overrite void StreamID.");
+ }
+ else if (hasStream(streamID)) {
+ throw new IllegalArgumentException("Stream already registered: " + streamID.name.toLowerCase());
+ }
+ }
+
+ protected ContentStream createStringStream(final StreamID streamID) {
+ ContentStream stream;
+ synchronized (registryCOWLock) {
+ testRegisterStream(streamID);
+ Map> idStreamMap = new IdentityHashMap>(this.idStreamMap);
+ Map> nameStreamMap = new HashMap>(this.nameStreamMap);
+ Map nameStreamIDMap = new HashMap(this.nameStreamIDMap);
+ stream = new DefaultContentStream(dispatcher);
+ idStreamMap.put(streamID, stream);
+ nameStreamMap.put(streamID.name.toLowerCase(), stream);
+ nameStreamIDMap.put(streamID.name.toLowerCase(), streamID);
+ this.idStreamMap = idStreamMap;
+ this.nameStreamMap = nameStreamMap;
+ this.nameStreamIDMap = nameStreamIDMap;
+
+ }
+ return stream;
+ }
+
+ /**
+ * A newly created id can be used here. For logging use existing ids always.
+ * @param loggerID
+ * @return
+ */
+ public boolean hasLogger(final LoggerID loggerID) {
+ return this.idNodeMap.containsKey(loggerID) || this.nameNodeMap.containsKey(loggerID.name.toLowerCase());
+ }
+
+ public boolean hasLogger(String name) {
+ return getLoggerID(name) != null;
+ }
+
+ /**
+ * Call under lock.
+ * @param loggerID
+ */
+ private void testRegisterLogger(final LoggerID loggerID) {
+ if (loggerID == null) {
+ throw new NullPointerException("LoggerID must not be null.");
+ }
+ else if (loggerID.name == null) {
+ throw new NullPointerException("LoggerID.name must not be null.");
+ }
+ else if (hasLogger(loggerID)) {
+ throw new IllegalArgumentException("Logger already registered: " + loggerID.name.toLowerCase());
+ }
+ }
+
+ /**
+ * Convenience method.
+ * @param logger
+ * @param options
+ * @return
+ */
+ protected LoggerID registerStringLogger(final ContentLogger logger, final LogOptions options) {
+ LoggerID loggerID = new LoggerID(options.name);
+ registerStringLogger(loggerID, logger, options);
+ return loggerID;
+ }
+
+ /**
+ * Convenience method.
+ * @param logger
+ * @param options
+ * @return
+ */
+ protected LoggerID registerStringLogger(final Logger logger, final LogOptions options) {
+ LoggerID loggerID = new LoggerID(options.name);
+ registerStringLogger(loggerID, logger, options);
+ return loggerID;
+ }
+
+ /**
+ * Convenience method.
+ * @param logger
+ * @param options
+ * @return
+ */
+ protected LoggerID registerStringLogger(final File file, final LogOptions options) {
+ LoggerID loggerID = new LoggerID(options.name);
+ registerStringLogger(loggerID, file, options);
+ return loggerID;
+ }
+
+ protected LogNode registerStringLogger(final LoggerID loggerID, final ContentLogger logger, final LogOptions options) {
+ LogNode node;
+ synchronized (registryCOWLock) {
+ testRegisterLogger(loggerID);
+ Map> idNodeMap = new IdentityHashMap>(this.idNodeMap);
+ Map> nameNodeMap = new HashMap>(this.nameNodeMap);
+ Map nameLoggerIDMap = new HashMap(this.nameLoggerIDMap);
+ node = new LogNode(loggerID, logger, options);
+ idNodeMap.put(loggerID, node);
+ nameNodeMap.put(loggerID.name.toLowerCase(), node);
+ nameLoggerIDMap.put(loggerID.name.toLowerCase(), loggerID);
+ this.idNodeMap = idNodeMap;
+ this.nameNodeMap = nameNodeMap;
+ this.nameLoggerIDMap = nameLoggerIDMap;
+ }
+ return node;
+ }
+
+ protected LogNode registerStringLogger(LoggerID loggerID, Logger logger, LogOptions options) {
+ LogNode node;
+ synchronized (registryCOWLock) {
+ LoggerAdapter adapter = new LoggerAdapter(logger); // Low cost.
+ // TODO: Store loggers too to prevent redundant registration.
+ node = registerStringLogger(loggerID, adapter, options);
+ }
+ return node;
+ }
+
+ protected LogNode registerStringLogger(LoggerID loggerID, File file, LogOptions options) {
+ LogNode node;
+ synchronized (registryCOWLock) {
+ testRegisterLogger(loggerID); // Redundant checking, because file loggers are expensive.
+ // TODO: Detect duplicate loggers (register same logger with given id and options).
+ FileLoggerAdapter adapter = new FileLoggerAdapter(file);
+ if (adapter.isInoperable()) {
+ adapter.detachLogger();
+ throw new RuntimeException("Failed to set up file logger for id '" + loggerID + "': " + file);
+ }
+ try {
+ node = registerStringLogger(loggerID, adapter, options);
+ } catch (Exception ex) {
+ // TODO: Exception is still bad.
+ adapter.detachLogger();
+ throw new RuntimeException(ex);
+ }
+ }
+ return node;
+ }
+
+ /**
+ * Attach a logger to a stream. Redundant attaching will mean no changes.
+ * @param loggerID Must exist.
+ * @param streamID Must exist.
+ */
+ protected void attachStringLogger(final LoggerID loggerID, final StreamID streamID) {
+ // TODO: More light-weight locking concept (thinking of dynamically changing per player streams)?
+ synchronized (registryCOWLock) {
+ if (!hasLogger(loggerID)) {
+ throw new RuntimeException("Logger is not registered: " + loggerID);
+ }
+ if (!hasStream(streamID)) {
+ // Note: This should also ensure the voidStreamID can't be used, because that one can't be registered.
+ throw new RuntimeException("Stream is not registered: " + streamID);
+ }
+ final LogNode node = idNodeMap.get(loggerID);
+ if (streamID == initStreamID) {
+ // TODO: Not sure about restrictions here. Could allow attaching other streams temporarily if other stuff is wanted.
+ switch(node.options.callContext) {
+ case PRIMARY_THREAD_ONLY:
+ case ANY_THREAD_DIRECT:
+ break;
+ default:
+ throw new RuntimeException("Unsupported call context for init stream " + streamID + ": " + node.options.callContext);
+ }
+ }
+ idStreamMap.get(streamID).addNode(node);
+ }
+ }
+
+ // TODO: Methods to replace options for loggers (+ loggers themselves)
+
+ // TODO: Later: attach streams to streams ? [few loggers: attach loggers rather]
+
+ // TODO: logger/stream: allow id lookup logger, file, etc. ?
+
+ /**
+ * Remove all loggers and streams including init, resulting in roughly the
+ * same state as is after calling the AbstractLogger constructor. Call from
+ * the primary thread (policy pending). If fallBackStreamID is set, it will be replaced by the init stream (if available) or by the void stream.
+ *
+ * @param msWaitFlush
+ */
+ protected void clear(final long msWaitFlush, final boolean recreateInitLogger) {
+ // TODO: enum (remove_all, recreate init, remap init, remap all to init)
+ synchronized (registryCOWLock) {
+ // Remove loggers from string streams.
+ for (ContentStream stream : idStreamMap.values()) {
+ stream.clear();
+ }
+ // Flush queues.
+ dispatcher.flush(msWaitFlush);
+ // Close/flush/shutdown string loggers, where possible, remove all from registry.
+ for (final LogNode node : idNodeMap.values()) {
+ if (node.logger instanceof FileLoggerAdapter) {
+ FileLoggerAdapter logger = (FileLoggerAdapter) node.logger;
+ logger.flush();
+ logger.detachLogger();
+ }
+ }
+ idNodeMap = new IdentityHashMap>();
+ nameNodeMap = new HashMap>();
+ nameLoggerIDMap = new HashMap();
+ // Remove string streams.
+ idStreamMap = new IdentityHashMap>();
+ nameStreamMap = new HashMap>();
+ nameStreamIDMap = new HashMap();
+ if (recreateInitLogger) {
+ createInitStream();
+ registerInitLogger();
+ if (fallBackStreamID != null && fallBackStreamID != voidStreamID) {
+ fallBackStreamID = initStreamID;
+ }
+ }
+ else if (fallBackStreamID != null) {
+ fallBackStreamID = voidStreamID;
+ }
+ }
+ }
+
+// /**
+// * Remove all registered streams and loggers, recreates init logger (and stream).
+// */
+// public void clear(final long msWaitFlush) {
+// clear(msWaitFlush, true);
+// }
+
+ /**
+ * Rather a graceful shutdown, including waiting for the asynchronous task, if necessary. Clear the registry. Also removes the init logger [subject to change].
+ * Call from the primary thread (policy pending).
+ */
+ public void shutdown() {
+ clear(500, false); // TODO: Policy / making sense.
+ }
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogNodeDispatcher.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogNodeDispatcher.java
new file mode 100644
index 00000000..77e9f4e7
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/AbstractLogNodeDispatcher.java
@@ -0,0 +1,232 @@
+package fr.neatmonster.nocheatplus.logging.details;
+
+import java.util.List;
+import java.util.logging.Level;
+
+import fr.neatmonster.nocheatplus.utilities.ds.corw.QueueRORA;
+
+/**
+ * Basic functionality, with int task ids, assuming a primary thread exists.
+ * @author dev1mc
+ *
+ */
+public abstract class AbstractLogNodeDispatcher implements LogNodeDispatcher { // TODO: Name
+
+ // TODO: Queues might need a drop policy with thresholds.
+ // TODO: Allow multiple tasks for logging, e.g. per file, also thinking of SQL logging. Could pool + round-robin.
+
+ /**
+ * This queue has to be processed in a task within the primary thread with calling runLogsPrimary.
+ */
+ protected final QueueRORA> queuePrimary = new QueueRORA>();
+ protected final QueueRORA> queueAsynchronous = new QueueRORA>();
+
+ /** Once a queue reaches this size, it will be reduced (loss of content). */
+ protected int maxQueueSize = 5000;
+
+ /**
+ * Task id, -1 means the asynchronous task is not running. Synchronize over
+ * queueAsynchronous. Must be maintained.
+ */
+ protected int taskAsynchronousID = -1;
+ /**
+ * Optional implementation for an asynchronous task, using the
+ * taskAsynchronousID, synchronized over queueAsynchronous.
+ */
+ protected final Runnable taskAsynchronous = new Runnable() {
+
+ @Override
+ public void run() {
+ // TODO: A more sophisticated System to allow "wake up on burst"?
+ int i = 0;
+ while (i < 3) {
+ if (runLogsAsynchronous()) {
+ i = 0;
+ if (taskAsynchronousID == -1) {
+ // Shutdown, hard return;
+ return;
+ }
+ Thread.yield();
+ } else {
+ i ++;
+ try {
+ Thread.sleep(25);
+ } catch (InterruptedException e) {
+ synchronized (queueAsynchronous) {
+ // Ensure re-scheduling can happen.
+ taskAsynchronousID = -1;
+ }
+ // TODO: throw?
+ return;
+ }
+ }
+ synchronized (queueAsynchronous) {
+ if (queueAsynchronous.isEmpty()) {
+ if (i >= 3) {
+ // Ensure re-scheduling can happen.
+ taskAsynchronousID = -1;
+ }
+ } else {
+ i = 0;
+ }
+ }
+ }
+ }
+
+ };
+
+ /**
+ * Optional init logger to log errors. Should log to the init stream, no queuing.
+ */
+ protected ContentLogger initLogger = null;
+
+ public void dispatch(LogNode node, Level level, C content) {
+ // TODO: Try/catch ?
+ if (isWithinContext(node)) {
+ node.logger.log(level, content);
+ } else {
+ scheduleLog(node, level, content);
+ }
+ }
+
+ protected boolean runLogsPrimary() {
+ return runLogs(queuePrimary);
+ }
+
+ protected boolean runLogsAsynchronous() {
+ return runLogs(queueAsynchronous);
+ }
+
+ private boolean runLogs(final QueueRORA> queue) {
+ // TODO: Consider allowYield + msYield, calling yield after 5 ms if async.
+ final List> records = queue.removeAll();
+ if (records.isEmpty()) {
+ return false;
+ }
+ for (final LogRecord> record : records) {
+ record.run();
+ }
+ return true;
+ }
+
+ @Override
+ public void flush(long ms) {
+ if (!isPrimaryThread()) {
+ // TODO: Could also switch policy to emptying the primary-thread queue if not called from within the primary thread.
+ throw new IllegalStateException("Must only be called from within the primary thread.");
+ }
+ // TODO: Note that all streams should be emptied here, except the fallback logger.
+
+ // Cancel task.
+ synchronized (queueAsynchronous) {
+ if (taskAsynchronousID != -1) {
+ // TODO: Allow queues to set to "no more input" ?
+ cancelTask(taskAsynchronousID);
+ taskAsynchronousID = -1;
+ } else {
+ // No need to wait.
+ ms = 0L;
+ }
+ }
+ if (ms > 0) {
+ try {
+ // TODO: Replace by a better concept.
+ Thread.sleep(ms);
+ } catch (InterruptedException e) {
+ // Ignore.
+ }
+ }
+
+ // Log the rest (from here logging should be done via the appropriate direct-only stream).
+ runLogsPrimary();
+ runLogsAsynchronous();
+ }
+
+ protected boolean isWithinContext(LogNode node) {
+ switch (node.options.callContext) {
+ case PRIMARY_THREAD_DIRECT:
+ case PRIMARY_THREAD_ONLY:
+ return isPrimaryThread();
+ case ANY_THREAD_DIRECT:
+ return true;
+ case ASYNCHRONOUS_ONLY:
+ case ASYNCHRONOUS_DIRECT:
+ return !isPrimaryThread();
+ case PRIMARY_THREAD_TASK:
+ case ASYNCHRONOUS_TASK:
+ // TODO: Each: Consider detecting if within that task (should rather be in case of re-scheduling?).
+ return false; // Always schedule (!).
+ default:
+ return false; // Force scheduling.
+ }
+ }
+
+ protected void scheduleLog(LogNode node, Level level, C content) {
+ final LogRecord record = new LogRecord(node, level, content); // TODO: parameters.
+ switch (node.options.callContext) {
+ case ASYNCHRONOUS_TASK:
+ case ASYNCHRONOUS_DIRECT:
+ if (queueAsynchronous.add(record) > maxQueueSize) {
+ reduceQueue(queueAsynchronous);
+ }
+ if (taskAsynchronousID == -1) { // Works, due to add being synchronized (not sure it's really better than full sync).
+ scheduleAsynchronous();
+ }
+ break;
+ case PRIMARY_THREAD_TASK:
+ case PRIMARY_THREAD_DIRECT:
+ queuePrimary.add(record);
+ // TODO: Consider re-scheduling policy ?
+ break;
+ case ASYNCHRONOUS_ONLY:
+ case PRIMARY_THREAD_ONLY:
+ // SKIP LOGGING.
+ break;
+ default:
+ // (ANY_THREAD_DIRECT never gets scheduled.)
+ throw new IllegalArgumentException("Bad CallContext: " + node.options.callContext);
+ }
+ }
+
+ /**
+ * Hard reduce the queue (heavy locking!).
+ * @param queue
+ */
+ private void reduceQueue(final QueueRORA> queue) {
+ // TODO: Different dropping strategies (drop first, last, alternate).
+ final int dropped;
+ synchronized (queue) {
+ final int size = queue.size();
+ if (size < maxQueueSize) {
+ // Never mind :).
+ return;
+ }
+ logINIT(Level.WARNING, "Dropping log entries from the " + (queue == queueAsynchronous ? "asynchronous" : "primary thread") + " queue to reduce memory consumption...");
+ dropped = queue.reduce(maxQueueSize / 2);
+ }
+ logINIT(Level.WARNING, "Dropped " + dropped + " log entries from the " + (queue == queueAsynchronous ? "asynchronous" : "primary thread") + " queue.");
+ }
+
+ @Override
+ public void setMaxQueueSize(int maxQueueSize) {
+ this.maxQueueSize = maxQueueSize;
+ }
+
+ @Override
+ public void setInitLogger(ContentLogger logger) {
+ this.initLogger = logger;
+ }
+
+ protected void logINIT(final Level level, final String message) {
+ if (initLogger != null) {
+ initLogger.log(level, message);
+ }
+ }
+
+ protected abstract boolean isPrimaryThread();
+
+ protected abstract void scheduleAsynchronous();
+
+ protected abstract void cancelTask(int taskId);
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentLogger.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentLogger.java
new file mode 100644
index 00000000..e5667751
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentLogger.java
@@ -0,0 +1,16 @@
+package fr.neatmonster.nocheatplus.logging.details;
+
+import java.util.logging.Level;
+
+/**
+ * Minimal interface for logging content to.
+ * @author dev1mc
+ *
+ * @param
+ */
+public interface ContentLogger {
+
+ // TODO: Not sure about generics.
+ public void log(Level level, C content);
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentStream.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentStream.java
new file mode 100644
index 00000000..83b6cb68
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/ContentStream.java
@@ -0,0 +1,30 @@
+package fr.neatmonster.nocheatplus.logging.details;
+
+
+/**
+ * Contracts:
+ * Intended usage: register=seldom, log=often
+ * Asynchronous access must be possible and fast, without locks (rather copy on write).
+ * @author dev1mc
+ *
+ * @param
+ */
+public interface ContentStream extends ContentLogger {
+
+ // TODO: Maybe also an abstract class.
+
+ // Maybe not: addFilter (filter away some stuff, e.g. by regex from config).
+
+ // TODO: Consider extra arguments for efficient registratioon with COWs.
+ public void addNode(LogNode node);
+
+ // addAdapter(ContentAdapter adapter) ? ID etc., attach to another stream.
+
+ // Removal and look up methods.
+
+ /**
+ * Remove all attached loggers and other.
+ */
+ public void clear();
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/DefaultContentStream.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/DefaultContentStream.java
new file mode 100644
index 00000000..fb97206f
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/DefaultContentStream.java
@@ -0,0 +1,51 @@
+package fr.neatmonster.nocheatplus.logging.details;
+
+import java.util.ArrayList;
+import java.util.logging.Level;
+
+/**
+ * Locking is done with the internal node list, log does not block (extra to whatever loggers would do).
+ * @author dev1mc
+ *
+ * @param
+ */
+public class DefaultContentStream implements ContentStream {
+
+ private ArrayList> nodes = new ArrayList>();
+
+ private final LogNodeDispatcher dispatcher;
+
+ public DefaultContentStream(LogNodeDispatcher dispatcher) {
+ this.dispatcher = dispatcher;
+ }
+
+ @Override
+ public void log(final Level level, final C content) {
+ final ArrayList> nodes = this.nodes;
+ for (int i = 0; i < nodes.size(); i++) {
+ dispatcher.dispatch(nodes.get(i), level, content);
+ }
+ }
+
+ @Override
+ public void addNode(LogNode node) {
+ // TODO: Consider throwing things at callers, in case of duplicate logger entries?
+ synchronized (nodes) {
+ if (this.nodes.contains(node)) {
+ // TODO: Consider throwing something.
+ return;
+ }
+ ArrayList> nodes = new ArrayList>(this.nodes);
+ nodes.add(node);
+ this.nodes = nodes;
+ }
+ }
+
+ @Override
+ public void clear() {
+ synchronized (nodes) {
+ nodes.clear();
+ }
+ }
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLogger.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLogger.java
new file mode 100644
index 00000000..20957106
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLogger.java
@@ -0,0 +1,143 @@
+package fr.neatmonster.nocheatplus.logging.details;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.logging.FileHandler;
+import java.util.logging.Formatter;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+import fr.neatmonster.nocheatplus.utilities.StringUtil;
+
+/**
+ * Wrap a file logger, adding convenience setup methods.
+ * @author dev1mc
+ *
+ */
+public class FileLogger {
+
+ protected static class FileLogFormatter extends Formatter {
+
+ private final SimpleDateFormat dateFormat = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
+
+ // TODO: Consider storing a custom line break (needs adding to StringUtil.throwableToString).
+
+ @Override
+ public String format(final LogRecord record) {
+ final Throwable throwable = record.getThrown();
+ final String message = record.getMessage();
+ int roughLength = 64 + message.length();
+ final String throwableMessage;
+ if (throwable != null) {
+ throwableMessage = StringUtil.throwableToString(throwable);
+ roughLength += throwableMessage.length();
+ } else {
+ throwableMessage = null;
+ }
+ final StringBuilder builder = new StringBuilder(roughLength);
+ builder.append(dateFormat.format(record.getMillis()));
+ builder.append(" [");
+ builder.append(record.getLevel().getLocalizedName().toUpperCase());
+ builder.append("] ");
+ builder.append(message);
+ builder.append('\n');
+ if (throwableMessage != null) {
+ builder.append(throwableMessage);
+ }
+ return builder.toString();
+ }
+
+ }
+
+ public final Logger logger;
+ protected FileHandler fileHandler = null;
+
+ protected boolean inoperable = false;
+
+ /**
+ * Initialize with an anonymous logger and the given log-file path.
+ *
+ * @param file Path to log file or an existing directory.
+ */
+ public FileLogger(File file) {
+ // TODO: Should re-add a file-name prefix (allow null).
+ // TODO: File encoding + line endings.
+ // TODO: Add options to switch file with file size, rolling files, etc.
+ // [could also keep track of rough size of written data, to switch file "on the fly", problem: which log? -> replace logger copy on write :p].
+ logger = Logger.getAnonymousLogger();
+ detachLogger();
+ final File container;
+ if (file.isDirectory()) {
+ container = file;
+ // Find a file name.
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yy-MM-dd");
+ String prefix = dateFormat.format(System.currentTimeMillis());
+ int n = 1;
+ while (true) {
+ file = new File(container, prefix + "-" + n + ".log");
+ if (!file.exists()) {
+ break;
+ }
+ n ++;
+ }
+ } else {
+ container = file.getParentFile();
+ }
+ // Ensure the container exists.
+ if (!container.exists()) {
+ container.mkdirs();
+ }
+ // Create file and initialize logger.
+ if (!file.exists()) {
+ try {
+ file.createNewFile();
+ } catch (IOException e) {
+ file = null;
+ }
+ }
+ if (file != null) {
+ try {
+ initLogger(file);
+ logger.log(Level.INFO, "Logger started.");
+ inoperable = false;
+ } catch (SecurityException e) {
+ } catch (IOException e) {
+ }
+ }
+
+ }
+
+ protected void initLogger(File file) throws SecurityException, IOException {
+ logger.setUseParentHandlers(false);
+ logger.setLevel(Level.ALL);
+ fileHandler = new FileHandler(file.getCanonicalPath(), true);
+ fileHandler.setLevel(Level.ALL);
+ fileHandler.setFormatter(new FileLogFormatter());
+ logger.addHandler(fileHandler);
+ }
+
+ public void flush() {
+ if (fileHandler != null) {
+ fileHandler.flush();
+ }
+ }
+
+ public void detachLogger() {
+ if (fileHandler != null) {
+ fileHandler.close();
+ fileHandler = null;
+ }
+ for (Handler handler : logger.getHandlers()) {
+ logger.removeHandler(handler);
+ }
+ }
+
+ public boolean isInoperable () {
+ return inoperable;
+ }
+
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLoggerAdapter.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLoggerAdapter.java
new file mode 100644
index 00000000..7b4eb63d
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/FileLoggerAdapter.java
@@ -0,0 +1,26 @@
+package fr.neatmonster.nocheatplus.logging.details;
+
+import java.io.File;
+import java.util.logging.Level;
+
+public class FileLoggerAdapter extends FileLogger implements ContentLogger {
+
+ // TODO: Store path/file either here or on FileLogger.
+
+ // TODO: Do store the string path (directory / direct file path), to reference correctly.
+
+ /**
+ * See FileLogger(File).
+ * @param file Path to log file or existing directory.
+ */
+ public FileLoggerAdapter(File file) {
+ super(file);
+ }
+
+ @Override
+ public void log(Level level, String content) {
+ // TODO: Check loggerisInoperable() ?
+ logger.log(level, content);
+ }
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNode.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNode.java
new file mode 100644
index 00000000..1470314d
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNode.java
@@ -0,0 +1,18 @@
+package fr.neatmonster.nocheatplus.logging.details;
+
+import fr.neatmonster.nocheatplus.logging.LoggerID;
+
+
+public class LogNode {
+
+ public final LoggerID loggerID;
+ public final ContentLogger logger;
+ public final LogOptions options;
+
+ public LogNode(LoggerID loggerID, ContentLogger logger, LogOptions options) {
+ this.loggerID = loggerID;
+ this.logger = logger;
+ this.options = new LogOptions(options);
+ }
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNodeDispatcher.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNodeDispatcher.java
new file mode 100644
index 00000000..d606558c
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogNodeDispatcher.java
@@ -0,0 +1,33 @@
+package fr.neatmonster.nocheatplus.logging.details;
+
+import java.util.logging.Level;
+
+/**
+ * Handle logging on LogNode instances (log directly, skip, schedule to log within tasks).
+ * @author dev1mc
+ *
+ */
+public interface LogNodeDispatcher { // TODO: Name.
+
+ public void dispatch(LogNode node, Level level, C content);
+
+ /**
+ * Cancel asynchronous tasks and remove all logs based on policy (log all or clear), default is to log
+ * all. Should be called from the primary thread, if it exists.
+ * @param ms Milliseconds to wait in case there is something being processed by asynchronous tasks.
+ */
+ public abstract void flush(long ms);
+
+ /**
+ * Allow to add a logger, for logging errors to the init stream. Must not use the queues. Can be null (no logging).
+ * @param logger
+ */
+ void setInitLogger(ContentLogger logger);
+
+ /**
+ * Set maximum queue size. After reaching that size queues will be reduced by dropping elements (asynchronous and primaray thread individually).
+ * @param maxQueueSize
+ */
+ void setMaxQueueSize(int maxQueueSize);
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogOptions.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogOptions.java
new file mode 100644
index 00000000..dff5e86b
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogOptions.java
@@ -0,0 +1,51 @@
+package fr.neatmonster.nocheatplus.logging.details;
+
+public class LogOptions {
+
+ /**
+ * Describe the context in which the log method may be used.
+ * Policies:
+ * ..._DIRECT: Will log directly if within context, otherwise schedule to a task.
+ * ..._TASK: Always schedule to a task.
+ *
+ * @author dev1mc
+ *
+ */
+ public static enum CallContext {
+ // TODO: Consider making this a general enum for call contexts (API wrappers, player data etc. too?).
+ // Primary as long as it exists...
+ /** Only execute directly in the primary thread, for other threads use a task to execute within the primary thread. */
+ PRIMARY_THREAD_DIRECT,
+ /** Always schedule to execute within the primary thread. */
+ PRIMARY_THREAD_TASK,
+ /** Only log if within the primary thread. */
+ PRIMARY_THREAD_ONLY,
+ /** Execute directly independently of thread. */
+ ANY_THREAD_DIRECT,
+ /** Ensure asynchronous logging, but avoid scheduling. */
+ ASYNCHRONOUS_DIRECT,
+ /** Always schedule to execute within a (more or less) dedicated asynchronous task. */
+ ASYNCHRONOUS_TASK,
+ /** Ensure it's logged asynchronously. */
+ ASYNCHRONOUS_ONLY,
+
+ // CUSTOM_THREAD_DIRECT|TASK // Needs a variable (Thread, methods to sync into a specific thread would have to be registered in LogManager).
+ ;
+
+ // TODO: Can distinguish further: Call from where, log from where directly, schedule from where (allow to skip certain contexts).
+ }
+
+ public final String name; // TODO: Name necessary ?
+ public final CallContext callContext;
+
+ public LogOptions(LogOptions options) {
+ this(options.name, options.callContext);
+ }
+
+ public LogOptions(String name, CallContext callContext) {
+ this.name = name;
+ this.callContext = callContext;
+ // TODO: shutdown policy (clear, log), rather with per-world threads.
+ }
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogRecord.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogRecord.java
new file mode 100644
index 00000000..76a3c213
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LogRecord.java
@@ -0,0 +1,28 @@
+package fr.neatmonster.nocheatplus.logging.details;
+
+import java.util.logging.Level;
+
+/**
+ * A log message to be executed within a task from a queue, hiding the generics.
+ * @author dev1mc
+ *
+ */
+public class LogRecord implements Runnable {
+
+ private final LogNode node;
+ private final Level level;
+ private final C content;
+
+ public LogRecord(LogNode node, Level level, C content) {
+ this.node = node;
+ this.level = level;
+ this.content = content;
+ }
+
+ @Override
+ public void run() {
+ // TODO: Checks / try-catch where?
+ node.logger.log(level, content);
+ }
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LoggerAdapter.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LoggerAdapter.java
new file mode 100644
index 00000000..d67abfc7
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/logging/details/LoggerAdapter.java
@@ -0,0 +1,19 @@
+package fr.neatmonster.nocheatplus.logging.details;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class LoggerAdapter implements ContentLogger {
+
+ protected final Logger logger;
+
+ public LoggerAdapter(Logger logger) {
+ this.logger = logger;
+ }
+
+ @Override
+ public void log(Level level, String content) {
+ logger.log(level, content);
+ }
+
+}
diff --git a/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/utilities/ds/corw/QueueRORA.java b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/utilities/ds/corw/QueueRORA.java
new file mode 100644
index 00000000..43dd847f
--- /dev/null
+++ b/NCPCommons/src/main/java/fr/neatmonster/nocheatplus/utilities/ds/corw/QueueRORA.java
@@ -0,0 +1,96 @@
+package fr.neatmonster.nocheatplus.utilities.ds.corw;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+
+/**
+ * A replace-on-read-all queue-thing, exchanging the internal list under lock by
+ * a new empty one, for a small locking time, so it is not really a typical
+ * copy-on-read. All methods use locking, the QueueRORA instance is used for
+ * locking.
+ *
+ * @author dev1mc
+ *
+ */
+public class QueueRORA {
+
+ private LinkedList elements = new LinkedList();
+
+ /**
+ * Add to list (synchronized).
+ * @param element
+ * @return Size of queue after adding.
+ */
+ public int add(final E element) {
+ final int size;
+ synchronized (this) {
+ elements.add(element);
+ size = elements.size();
+ }
+ return size;
+ }
+
+ /**
+ *
+ * @return An ordinary (linked) List containing all elements.
+ */
+ public List removeAll() {
+ final List result;
+ synchronized (this) {
+ result = elements;
+ elements = new LinkedList();
+ }
+ return result;
+ }
+
+ /**
+ * Remove oldest entries until maxSize is reached.
+ * @param maxSize
+ * @return
+ */
+ public int reduce(final int maxSize) {
+ int dropped = 0;
+ synchronized (this) {
+ final int size = elements.size();
+ if (size <= maxSize) {
+ return dropped;
+ }
+ while (dropped < size - maxSize) {
+ elements.removeFirst();
+ dropped ++;
+ }
+ }
+ return dropped;
+ }
+
+ public void clear() {
+ removeAll();
+ }
+
+ /**
+ *
+ * @return
+ */
+ public boolean isEmpty() {
+ final boolean isEmpty;
+ synchronized (this) {
+ isEmpty = elements.isEmpty();
+ }
+ return isEmpty;
+ }
+
+ /**
+ *
+ * @return
+ */
+ public int size() {
+ final int size;
+ synchronized (this) {
+ size = elements.size();
+ }
+ return size;
+ }
+
+}
diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/config/DefaultConfig.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/config/DefaultConfig.java
index b2faf641..1f64c398 100644
--- a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/config/DefaultConfig.java
+++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/config/DefaultConfig.java
@@ -36,6 +36,7 @@ public class DefaultConfig extends ConfigFile {
// not set(ConfPaths.CONFIGVERSION_SAVED, -1);
set(ConfPaths.LOGGING_ACTIVE, true);
set(ConfPaths.LOGGING_DEBUG, false);
+ set(ConfPaths.LOGGING_MAXQUEUESIZE, 5000);
set(ConfPaths.LOGGING_BACKEND_CONSOLE_ACTIVE, true);
set(ConfPaths.LOGGING_BACKEND_CONSOLE_PREFIX, "[NoCheatPlus] ");
set(ConfPaths.LOGGING_BACKEND_FILE_ACTIVE, true);
diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/LogManager.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/LogManager.java
new file mode 100644
index 00000000..845168af
--- /dev/null
+++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/LogManager.java
@@ -0,0 +1,154 @@
+package fr.neatmonster.nocheatplus.logging;
+
+import java.io.File;
+import java.util.logging.Level;
+
+import org.bukkit.Bukkit;
+import org.bukkit.plugin.Plugin;
+
+import fr.neatmonster.nocheatplus.NCPAPIProvider;
+import fr.neatmonster.nocheatplus.config.ConfPaths;
+import fr.neatmonster.nocheatplus.config.ConfigFile;
+import fr.neatmonster.nocheatplus.config.ConfigManager;
+import fr.neatmonster.nocheatplus.logging.details.AbstractLogManager;
+import fr.neatmonster.nocheatplus.logging.details.BukkitLogNodeDispatcher;
+import fr.neatmonster.nocheatplus.logging.details.ContentLogger;
+import fr.neatmonster.nocheatplus.logging.details.FileLoggerAdapter;
+import fr.neatmonster.nocheatplus.logging.details.LogOptions;
+import fr.neatmonster.nocheatplus.logging.details.LogOptions.CallContext;
+
+
+/**
+ * Central access point for logging. The default loggers use the stream names,
+ * at least as prefixes).
+ * Note that logging to the init/plugin/server with debug/fine or finer, might
+ * result in the server loggers suppressing those. As long as default file is
+ * activated, logging to init will log all levels to the file.
+ *
+ * @author dev1mc
+ *
+ */
+public class LogManager extends AbstractLogManager {
+
+ // TODO: Make LogManager an interface <- AbstractLogManager <- BukkitLogManager (hide some / instanceof).
+
+ // TODO: ingame logging [ingame needs api to keep track of players who receive notifications.].
+ // TODO: Later: Custom loggers (file, other), per-player-streams (debug per player), custom ingame loggers (one or more players).
+
+ protected final Plugin plugin;
+
+ /**
+ * This will create all default loggers as well.
+ * @param plugin
+ */
+ public LogManager(Plugin plugin) {
+ super(new BukkitLogNodeDispatcher(plugin), Streams.defaultPrefix, Streams.INIT);
+ this.plugin = plugin;
+ ConfigFile config = ConfigManager.getConfigFile();
+ createDefaultLoggers(config);
+ getLogNodeDispatcher().setMaxQueueSize(config.getInt(ConfPaths.LOGGING_MAXQUEUESIZE));
+ }
+
+ @Override
+ protected void registerInitLogger() {
+ synchronized (registryCOWLock) {
+ if (!hasStream(Streams.INIT)) {
+ createInitStream();
+ }
+ else if (hasLogger("init")) {
+ // Shallow check.
+ return;
+ }
+ // Attach a new restrictive init logger.
+ // TODO: If thread-safe use ANY_THREAD_DIRECT (should then allow to interrupt).
+ LoggerID initLoggerID = registerStringLogger(new ContentLogger() {
+
+ @Override
+ public void log(Level level, String content) {
+ try {
+ Bukkit.getLogger().log(level, content);
+ } catch (Throwable t) {}
+ }
+
+ }, new LogOptions(Streams.INIT.name, CallContext.PRIMARY_THREAD_ONLY));
+ attachStringLogger(initLoggerID, Streams.INIT);
+ }
+ }
+
+ /**
+ * Create default loggers and streams.
+ */
+ protected void createDefaultLoggers(ConfigFile config) {
+ for (StreamID streamID : new StreamID[] {
+ Streams.SERVER_LOGGER, Streams.PLUGIN_LOGGER,
+ Streams.NOTIFY_INGAME,
+ Streams.DEFAULT_FILE, Streams.TRACE_FILE,
+
+ }) {
+ createStringStream(streamID);
+ }
+ // TODO: Consult configuration and/or detect what options can or want to be used here.
+ CallContext bukkitLoggerContext = CallContext.PRIMARY_THREAD_TASK; // TODO: Config + individually.
+ LoggerID tempID;
+
+ // Server logger.
+ tempID = registerStringLogger(new ContentLogger() {
+
+ @Override
+ public void log(Level level, String content) {
+ Bukkit.getLogger().log(level, content);
+ }
+
+ }, new LogOptions(Streams.SERVER_LOGGER.name, bukkitLoggerContext));
+
+ // Plugin logger.
+ tempID = registerStringLogger(plugin.getLogger(), new LogOptions(Streams.PLUGIN_LOGGER.name, bukkitLoggerContext));
+ attachStringLogger(tempID, Streams.PLUGIN_LOGGER);
+
+ // Ingame logger (assume not thread-safe at first).
+ tempID = registerStringLogger(new ContentLogger() {
+
+ @Override
+ public void log(Level level, String content) {
+ // Ignore level for now.
+ NCPAPIProvider.getNoCheatPlusAPI().sendAdminNotifyMessage(content);
+ }
+
+ }, new LogOptions(Streams.NOTIFY_INGAME.name, CallContext.PRIMARY_THREAD_DIRECT));
+ attachStringLogger(tempID, Streams.NOTIFY_INGAME);
+
+ File file;
+ // TODO: Allow absolute paths ?
+ // Default file logger.
+ file = new File(plugin.getDataFolder(), config.getString(ConfPaths.LOGGING_BACKEND_FILE_FILENAME));
+ ContentLogger defaultFileLogger = new FileLoggerAdapter(file);
+ tempID = registerStringLogger(defaultFileLogger, new LogOptions(Streams.DEFAULT_FILE.name, CallContext.ASYNCHRONOUS_DIRECT));
+ attachStringLogger(tempID, Streams.DEFAULT_FILE);
+ // Attach default file logger to init too, to log something, even if asynchronous, direct from any thread.
+ tempID = registerStringLogger(defaultFileLogger, new LogOptions(Streams.DEFAULT_FILE.name +".init", CallContext.ANY_THREAD_DIRECT));
+ attachStringLogger(tempID, Streams.INIT);
+
+ // Trace file logger.
+ // TODO: Create a real if "needed", dedicated file.
+ attachStringLogger(getLoggerID(Streams.DEFAULT_FILE.name), Streams.TRACE_FILE); // Direct to default file for now.
+
+ }
+
+ /**
+ * Not "official". TODO: Hide.
+ */
+ public void onReload() {
+ // Hard clear and re-do loggers. Might result in loss of content.
+ clear(0L, true); // Can not afford to wait.
+ createDefaultLoggers(ConfigManager.getConfigFile());
+ }
+
+ /**
+ * Necessary logging to a primary thread task (TickTask).
+ */
+ public void startTasks() {
+ // TODO: Schedule / hide.
+ ((BukkitLogNodeDispatcher) getLogNodeDispatcher()).startTasks();
+ }
+
+}
diff --git a/NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/details/BukkitLogNodeDispatcher.java b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/details/BukkitLogNodeDispatcher.java
new file mode 100644
index 00000000..f041a1c6
--- /dev/null
+++ b/NCPCore/src/main/java/fr/neatmonster/nocheatplus/logging/details/BukkitLogNodeDispatcher.java
@@ -0,0 +1,69 @@
+package fr.neatmonster.nocheatplus.logging.details;
+
+import java.util.logging.Level;
+
+import org.bukkit.Bukkit;
+import org.bukkit.plugin.Plugin;
+
+import fr.neatmonster.nocheatplus.components.TickListener;
+import fr.neatmonster.nocheatplus.utilities.TickTask;
+
+public class BukkitLogNodeDispatcher extends AbstractLogNodeDispatcher { // TODO: Name.
+
+
+ /**
+ * Permanent TickListener for logging [TODO: on-demand scheduling, but thread-safe. With extra lock.]
+ */
+ private final TickListener taskPrimary = new TickListener() {
+
+ @Override
+ public void onTick(final int tick, final long timeLast) {
+ if (runLogsPrimary()) {
+ // TODO: Here or within runLogsPrimary, handle rescheduling.
+ }
+ }
+
+ };
+
+ /**
+ * Needed for scheduling.
+ */
+ private final Plugin plugin;
+
+ public BukkitLogNodeDispatcher(Plugin plugin) {
+ this.plugin = plugin;
+ }
+
+ public void startTasks() {
+ // TODO: This is a temporary solution. Needs on-demand scheduling [or a wrapper task].
+ TickTask.addTickListener(taskPrimary);
+ }
+
+ @Override
+ protected void logINIT(Level level, String message) {
+ // TODO: This is cheating !
+
+ }
+
+ @Override
+ protected void scheduleAsynchronous() {
+ synchronized (queueAsynchronous) {
+ if (taskAsynchronousID == -1) {
+ // Deadlocking should not be possible.
+ taskAsynchronousID = Bukkit.getScheduler().runTaskAsynchronously(plugin, taskAsynchronous).getTaskId();
+ // TODO: Re-check task id here.
+ }
+ }
+ }
+
+ @Override
+ protected final boolean isPrimaryThread() {
+ return Bukkit.isPrimaryThread();
+ }
+
+ @Override
+ protected void cancelTask(int taskId) {
+ Bukkit.getScheduler().cancelTask(taskId);
+ }
+
+}