replace singletons

main
jbb01 10 months ago committed by jbb01
parent eadf1eaf5b
commit 0477142233

@ -14,7 +14,7 @@ import java.util.List;
public interface ChatBotSupportMXBean { public interface ChatBotSupportMXBean {
@SneakyThrows @SneakyThrows
static @NotNull ObjectName getObjectName(String name) { static @NotNull ObjectName getObjectName(@NotNull String name) {
return ObjectName.getInstance(STR."eu.jonahbauer.chat.bot.server:component=ChatBots,name=\{quote(name)}"); return ObjectName.getInstance(STR."eu.jonahbauer.chat.bot.server:component=ChatBots,name=\{quote(name)}");
} }

@ -1,30 +1,32 @@
package eu.jonahbauer.chat.server; package eu.jonahbauer.chat.server;
import eu.jonahbauer.chat.server.bot.ChatBotSupervisor; import eu.jonahbauer.chat.server.bot.ChatBotSupervisor;
import eu.jonahbauer.chat.server.management.impl.ChatBotManager;
import eu.jonahbauer.chat.server.management.impl.SocketManager;
import eu.jonahbauer.chat.server.socket.SocketSupervisor; import eu.jonahbauer.chat.server.socket.SocketSupervisor;
import eu.jonahbauer.chat.server.util.Lazy.MutableLazy;
import javax.management.JMException;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
public class Main { public class Main {
public static void main(String[] args) throws IOException, InterruptedException { public static void main(String[] args) throws IOException, InterruptedException, JMException {
// initialize ChatBotManager and SocketManager
var _ = ChatBotManager.INSTANCE;
var _ = SocketManager.INSTANCE;
var config = Config.load(); var config = Config.load();
ChatBotSupervisor.INSTANCE.start(config); var chatBotSupervisorLazy = new MutableLazy<ChatBotSupervisor>();
SocketSupervisor.INSTANCE.start(config); var socketSupervisorLazy = new MutableLazy<SocketSupervisor>();
try (
var chatBotSupervisor = chatBotSupervisorLazy.set(new ChatBotSupervisor(socketSupervisorLazy));
var socketSupervisor = socketSupervisorLazy.set(new SocketSupervisor(chatBotSupervisorLazy))
) {
chatBotSupervisor.start(config);
socketSupervisor.start(config);
try { try {
// keep main thread running // keep main thread running
new CountDownLatch(1).await(); new CountDownLatch(1).await();
} catch (InterruptedException e) { } catch (InterruptedException _) {
SocketSupervisor.INSTANCE.stop(); // ignore
ChatBotSupervisor.INSTANCE.stop(); }
} }
} }
} }

@ -6,32 +6,43 @@ import eu.jonahbauer.chat.bot.config.BotConfig;
import eu.jonahbauer.chat.bot.impl.BotCreationException; import eu.jonahbauer.chat.bot.impl.BotCreationException;
import eu.jonahbauer.chat.bot.impl.ChatBotFactory; import eu.jonahbauer.chat.bot.impl.ChatBotFactory;
import eu.jonahbauer.chat.server.Config; import eu.jonahbauer.chat.server.Config;
import eu.jonahbauer.chat.server.management.impl.ChatBotManager;
import eu.jonahbauer.chat.server.management.impl.ChatBotSupport; import eu.jonahbauer.chat.server.management.impl.ChatBotSupport;
import eu.jonahbauer.chat.server.socket.SocketSupervisor; import eu.jonahbauer.chat.server.socket.SocketSupervisor;
import eu.jonahbauer.chat.server.util.Lazy;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.Blocking; import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NonBlocking; import org.jetbrains.annotations.NonBlocking;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import javax.management.JMException;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.*;
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
@Log4j2 @Log4j2
public enum ChatBotSupervisor { public final class ChatBotSupervisor implements AutoCloseable {
INSTANCE;
private static final Duration POST_EXPIRATION = Duration.ofSeconds(2); private static final Duration POST_EXPIRATION = Duration.ofSeconds(2);
private static final int MESSAGE_QUEUE_SIZE = 10; private static final int MESSAGE_QUEUE_SIZE = 10;
/** /**
* Map of running bots indexed by their name. * Map of running bots indexed by their name.
*/ */
private final @NotNull Map<@NotNull String, @NotNull RunningBot> bots = new HashMap<>(); private final @NotNull ConcurrentMap<@NotNull String, @NotNull RunningBot> bots = new ConcurrentHashMap<>();
private final @NotNull ReentrantLock lock = new ReentrantLock();
private final @NotNull Lazy<SocketSupervisor> socketSupervisor;
public ChatBotSupervisor(@NotNull Lazy<SocketSupervisor> socketSupervisor) throws JMException {
ChatBotManager.init(this);
this.socketSupervisor = socketSupervisor;
}
/** /**
* Dispatches the message to all running bots. If a bot has too many messages queued up, the message may not be * Dispatches the message to all running bots. If a bot has too many messages queued up, the message may not be
@ -52,9 +63,16 @@ public enum ChatBotSupervisor {
* @param config a configuration * @param config a configuration
* @throws IllegalStateException if any bots are running already * @throws IllegalStateException if any bots are running already
*/ */
public synchronized void start(@NotNull Config config) { public void start(@NotNull Config config) {
if (!bots.isEmpty()) throw new IllegalStateException("start(Config) may not be used when any bots are running already"); lock.lock();
config.bots().forEach(this::start); try {
if (!bots.isEmpty()) {
throw new IllegalStateException("start(Config) may not be used when any bots are running already");
}
config.bots().forEach(this::start);
} finally {
lock.unlock();
}
} }
/** /**
@ -64,7 +82,7 @@ public enum ChatBotSupervisor {
* @param channels the channels the bot will be active in * @param channels the channels the bot will be active in
* @throws BotCreationException if the bot could not be created * @throws BotCreationException if the bot could not be created
*/ */
public synchronized void start(@NotNull String name, @NotNull String type, @NotNull List<String> channels) { public void start(@NotNull String name, @NotNull String type, @NotNull List<String> channels) {
start(name, BotConfig.builder().type(type).channels(channels).build()); start(name, BotConfig.builder().type(type).channels(channels).build());
} }
@ -74,9 +92,16 @@ public enum ChatBotSupervisor {
* @param config the bot configuration * @param config the bot configuration
* @throws BotCreationException if the bot could not be created * @throws BotCreationException if the bot could not be created
*/ */
public synchronized void start(@NotNull String name, @NotNull BotConfig config) { public void start(@NotNull String name, @NotNull BotConfig config) {
if (bots.containsKey(name)) throw new BotCreationException("Duplicate bot name: " + name); lock.lock();
bots.put(name, new RunningBot(name, config)); try {
if (bots.containsKey(name)) {
throw new BotCreationException("Duplicate bot name: " + name);
}
bots.put(name, new RunningBot(name, config));
} finally {
lock.unlock();
}
} }
/** /**
@ -87,10 +112,13 @@ public enum ChatBotSupervisor {
public void stop(@NotNull String name) throws InterruptedException { public void stop(@NotNull String name) throws InterruptedException {
RunningBot bot; RunningBot bot;
synchronized (this) { lock.lock();
try {
bot = bots.remove(name); bot = bots.remove(name);
if (bot == null) return; if (bot == null) return;
bot.stop(); bot.stop();
} finally {
lock.unlock();
} }
bot.join(); bot.join();
@ -103,12 +131,15 @@ public enum ChatBotSupervisor {
public void stop() throws InterruptedException { public void stop() throws InterruptedException {
var stopped = new ArrayList<RunningBot>(); var stopped = new ArrayList<RunningBot>();
synchronized (this) { lock.lock();
try {
for (RunningBot bot : bots.values()) { for (RunningBot bot : bots.values()) {
stopped.add(bot); stopped.add(bot);
bot.stop(); bot.stop();
} }
bots.clear(); bots.clear();
} finally {
lock.unlock();
} }
for (var bot : stopped) { for (var bot : stopped) {
@ -116,6 +147,11 @@ public enum ChatBotSupervisor {
} }
} }
@Override
public void close() throws InterruptedException {
stop();
}
/** /**
* {@return a map of all currently running bots and their effective config} * {@return a map of all currently running bots and their effective config}
*/ */
@ -144,10 +180,10 @@ public enum ChatBotSupervisor {
/** /**
* Manages a {@link ChatBot} instance running on its own thread. Takes care of recreating the bot after it threw * Manages a {@link ChatBot} instance running on its own thread. Takes care of recreating the bot after it threw
* an exception. {@linkplain ChatBotSupport#register(String, BotConfig) Registers} the bot during construction * an exception. {@linkplain ChatBotSupport#register(ChatBotSupervisor, String, BotConfig) Registers} the bot
* and {@linkplain ChatBotSupport#unregister(String) unregisters} it during {@link #stop()}. * during construction and {@linkplain ChatBotSupport#unregister(String) unregisters} it during {@link #stop()}.
*/ */
private static class RunningBot implements Runnable { private class RunningBot implements Runnable {
private final @NotNull String name; private final @NotNull String name;
private final @NotNull BotConfig config; private final @NotNull BotConfig config;
private final @NotNull ChatBotFactory<?> factory; private final @NotNull ChatBotFactory<?> factory;
@ -163,7 +199,7 @@ public enum ChatBotSupervisor {
this.factory = getChatBotFactory(config.getType()); this.factory = getChatBotFactory(config.getType());
log.info("starting bot {}...", name); log.info("starting bot {}...", name);
ChatBotSupport.register(name, config); ChatBotSupport.register(ChatBotSupervisor.this, name, config);
this.thread = Thread.ofVirtual().name("ChatBot[" + name + "]").start(this); this.thread = Thread.ofVirtual().name("ChatBot[" + name + "]").start(this);
} }
@ -200,7 +236,7 @@ public enum ChatBotSupervisor {
} else if (!bot.getConfig().getChannels().contains(post.post().channel())) { } else if (!bot.getConfig().getChannels().contains(post.post().channel())) {
log.debug("Ignoring message {} because channel is not listened to.", post.post()); log.debug("Ignoring message {} because channel is not listened to.", post.post());
} else { } else {
bot.onMessage(SocketSupervisor.INSTANCE, post.post()); bot.onMessage(ChatBotSupervisor.this.socketSupervisor.get(), post.post());
} }
} catch (InterruptedException _) { } catch (InterruptedException _) {
} }

@ -5,6 +5,8 @@ import eu.jonahbauer.chat.server.management.annotations.ManagedBean;
import eu.jonahbauer.chat.server.management.annotations.ManagedOperation; import eu.jonahbauer.chat.server.management.annotations.ManagedOperation;
import eu.jonahbauer.chat.server.management.annotations.ManagedParameter; import eu.jonahbauer.chat.server.management.annotations.ManagedParameter;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.management.*; import javax.management.*;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@ -15,13 +17,13 @@ import java.util.Locale;
import java.util.function.Function; import java.util.function.Function;
@Log4j2 @Log4j2
public class AdvancedMBean extends StandardMBean { class AdvancedMBean extends StandardMBean {
public <T> AdvancedMBean(T implementation) { public AdvancedMBean(@NotNull Object implementation) {
super(implementation, null, true); super(implementation, null, true);
} }
@Override @Override
protected String getDescription(MBeanInfo info) { protected @Nullable String getDescription(@Nullable MBeanInfo info) {
var clazz = getMBeanInterface(); var clazz = getMBeanInterface();
var annotation = clazz.getAnnotation(ManagedBean.class); var annotation = clazz.getAnnotation(ManagedBean.class);
if (annotation != null && !"".equals(annotation.description())) { if (annotation != null && !"".equals(annotation.description())) {
@ -32,9 +34,7 @@ public class AdvancedMBean extends StandardMBean {
} }
@Override @Override
protected int getImpact(MBeanOperationInfo info) { protected int getImpact(@Nullable MBeanOperationInfo info) {
if (info == null) return MBeanOperationInfo.UNKNOWN;
var method = findOperation(info); var method = findOperation(info);
var annotation = method != null ? method.getAnnotation(ManagedOperation.class) : null; var annotation = method != null ? method.getAnnotation(ManagedOperation.class) : null;
if (annotation != null) { if (annotation != null) {
@ -45,9 +45,7 @@ public class AdvancedMBean extends StandardMBean {
} }
@Override @Override
protected String getDescription(MBeanOperationInfo info) { protected @Nullable String getDescription(@Nullable MBeanOperationInfo info) {
if (info == null) return null;
var method = findOperation(info); var method = findOperation(info);
var annotation = method != null ? method.getAnnotation(ManagedOperation.class) : null; var annotation = method != null ? method.getAnnotation(ManagedOperation.class) : null;
if (annotation != null && !"".equals(annotation.description())) { if (annotation != null && !"".equals(annotation.description())) {
@ -58,13 +56,9 @@ public class AdvancedMBean extends StandardMBean {
} }
@Override @Override
protected String getParameterName(MBeanOperationInfo op, MBeanParameterInfo param, int sequence) { protected @Nullable String getParameterName(@Nullable MBeanOperationInfo op, @Nullable MBeanParameterInfo param, int sequence) {
if (op == null) return super.getParameterName(op, param, sequence);
var method = findOperation(op); var method = findOperation(op);
if (method == null) return super.getParameterName(op, param, sequence); var annotation = method != null ? method.getParameters()[sequence].getAnnotation(ManagedParameter.class) : null;
var annotation = method.getParameters()[sequence].getAnnotation(ManagedParameter.class);
if (annotation != null && !"".equals(annotation.name())) { if (annotation != null && !"".equals(annotation.name())) {
return annotation.name(); return annotation.name();
} else { } else {
@ -73,13 +67,9 @@ public class AdvancedMBean extends StandardMBean {
} }
@Override @Override
protected String getDescription(MBeanOperationInfo op, MBeanParameterInfo param, int sequence) { protected @Nullable String getDescription(@Nullable MBeanOperationInfo op, @Nullable MBeanParameterInfo param, int sequence) {
if (op == null) return super.getParameterName(op, param, sequence);
var method = findOperation(op); var method = findOperation(op);
if (method == null) return super.getParameterName(op, param, sequence); var annotation = method != null ? method.getParameters()[sequence].getAnnotation(ManagedParameter.class) : null;
var annotation = method.getParameters()[sequence].getAnnotation(ManagedParameter.class);
if (annotation != null && !"".equals(annotation.description())) { if (annotation != null && !"".equals(annotation.description())) {
return annotation.description(); return annotation.description();
} else { } else {
@ -88,7 +78,9 @@ public class AdvancedMBean extends StandardMBean {
} }
@Override @Override
protected String getDescription(MBeanAttributeInfo info) { protected @Nullable String getDescription(@Nullable MBeanAttributeInfo info) {
if (info == null) return super.getDescription(info);
var name = info.getName(); var name = info.getName();
var type = getType(info, MBeanAttributeInfo::getType); var type = getType(info, MBeanAttributeInfo::getType);
@ -107,7 +99,7 @@ public class AdvancedMBean extends StandardMBean {
return super.getDescription(info); return super.getDescription(info);
} }
private <T extends MBeanFeatureInfo> String getType(T info, Function<T, String> getType) { private <T extends MBeanFeatureInfo> String getType(@NotNull T info, @NotNull Function<T, String> getType) {
var descriptor = info.getDescriptor(); var descriptor = info.getDescriptor();
if (descriptor == null) return getType.apply(info); if (descriptor == null) return getType.apply(info);
@ -119,7 +111,9 @@ public class AdvancedMBean extends StandardMBean {
} }
} }
private Method findOperation(MBeanOperationInfo info) { private @Nullable Method findOperation(@Nullable MBeanOperationInfo info) {
if (info == null) return null;
var name = info.getName(); var name = info.getName();
var params = new ArrayList<String>(); var params = new ArrayList<String>();
@ -130,11 +124,11 @@ public class AdvancedMBean extends StandardMBean {
return findMethod(name, params); return findMethod(name, params);
} }
private Method findMethod(String name, String...parameters) { private @Nullable Method findMethod(@NotNull String name, @NotNull String @NotNull... parameters) {
return findMethod(name, Arrays.asList(parameters)); return findMethod(name, Arrays.asList(parameters));
} }
private Method findMethod(String name, List<String> parameters) { private @Nullable Method findMethod(@NotNull String name, @NotNull List<@NotNull String> parameters) {
var clazz = getMBeanInterface(); var clazz = getMBeanInterface();
methods: for (var method : clazz.getMethods()) { methods: for (var method : clazz.getMethods()) {
if (!method.getName().equals(name)) continue; if (!method.getName().equals(name)) continue;
@ -150,7 +144,7 @@ public class AdvancedMBean extends StandardMBean {
return null; return null;
} }
private static String capitalize(String name) { private static @NotNull String capitalize(@NotNull String name) {
if (name.isEmpty()) return name; if (name.isEmpty()) return name;
return name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1); return name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1);
} }

@ -5,26 +5,31 @@ import eu.jonahbauer.chat.bot.impl.ChatBotFactory;
import eu.jonahbauer.chat.server.bot.ChatBotSupervisor; import eu.jonahbauer.chat.server.bot.ChatBotSupervisor;
import eu.jonahbauer.chat.server.management.BotConfigSupport; import eu.jonahbauer.chat.server.management.BotConfigSupport;
import eu.jonahbauer.chat.server.management.ChatBotManagerMXBean; import eu.jonahbauer.chat.server.management.ChatBotManagerMXBean;
import lombok.SneakyThrows;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
import javax.management.JMException;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public enum ChatBotManager implements ChatBotManagerMXBean { public final class ChatBotManager implements ChatBotManagerMXBean {
INSTANCE; private final @NotNull ChatBotSupervisor supervisor;
@SneakyThrows public static void init(@NotNull ChatBotSupervisor supervisor) throws JMException {
ChatBotManager() { var impl = new ChatBotManager(supervisor);
var server = ManagementFactory.getPlatformMBeanServer(); var server = ManagementFactory.getPlatformMBeanServer();
server.registerMBean(new AdvancedMBean(this), ChatBotManagerMXBean.NAME); server.registerMBean(new AdvancedMBean(impl), ChatBotManagerMXBean.NAME);
}
private ChatBotManager(@NotNull ChatBotSupervisor supervisor) {
this.supervisor = Objects.requireNonNull(supervisor);
} }
@Override @Override
public void start(@NotNull String name, @NotNull String type, @NotNull List<@NotNull String> channels) { public void start(@NotNull String name, @NotNull String type, @NotNull List<@NotNull String> channels) {
try { try {
ChatBotSupervisor.INSTANCE.start(name, type, channels); supervisor.start(name, type, channels);
} catch (BotCreationException ex) { } catch (BotCreationException ex) {
throw new IllegalArgumentException(ex.getMessage(), ex.getCause()); throw new IllegalArgumentException(ex.getMessage(), ex.getCause());
} }
@ -33,7 +38,7 @@ public enum ChatBotManager implements ChatBotManagerMXBean {
@Override @Override
public void start(@NotNull String name, @NotNull BotConfigSupport config) { public void start(@NotNull String name, @NotNull BotConfigSupport config) {
try { try {
ChatBotSupervisor.INSTANCE.start(name, config.unwrap()); supervisor.start(name, config.unwrap());
} catch (BotCreationException ex) { } catch (BotCreationException ex) {
throw new IllegalArgumentException(ex.getMessage(), ex.getCause()); throw new IllegalArgumentException(ex.getMessage(), ex.getCause());
} }
@ -41,23 +46,23 @@ public enum ChatBotManager implements ChatBotManagerMXBean {
@Override @Override
public void stop(@NotNull String name) throws InterruptedException { public void stop(@NotNull String name) throws InterruptedException {
ChatBotSupervisor.INSTANCE.stop(name); supervisor.stop(name);
} }
@Override @Override
public void stop() throws InterruptedException { public void stop() throws InterruptedException {
ChatBotSupervisor.INSTANCE.stop(); supervisor.stop();
} }
@Override @Override
public @NotNull Map<@NotNull String, @NotNull BotConfigSupport> getBots() { public @Unmodifiable @NotNull Map<@NotNull String, @NotNull BotConfigSupport> getBots() {
var out = new TreeMap<String, BotConfigSupport>(); var out = new TreeMap<String, BotConfigSupport>();
ChatBotSupervisor.INSTANCE.getBots().forEach((key, value) -> out.put(key, new BotConfigSupport(value))); supervisor.getBots().forEach((key, value) -> out.put(key, new BotConfigSupport(value)));
return Collections.unmodifiableMap(out); return Collections.unmodifiableMap(out);
} }
@Override @Override
public @NotNull SortedSet<@NotNull String> getBotImplementations() { public @Unmodifiable @NotNull SortedSet<@NotNull String> getBotImplementations() {
return Collections.unmodifiableSortedSet( return Collections.unmodifiableSortedSet(
ChatBotFactory.implementations().stream() ChatBotFactory.implementations().stream()
.map(Class::getCanonicalName) .map(Class::getCanonicalName)

@ -6,25 +6,29 @@ import eu.jonahbauer.chat.server.management.BotConfigSupport;
import eu.jonahbauer.chat.server.management.ChatBotSupportMXBean; import eu.jonahbauer.chat.server.management.ChatBotSupportMXBean;
import lombok.Getter; import lombok.Getter;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.util.Objects;
@Getter @Getter
@Log4j2 @Log4j2
public class ChatBotSupport implements ChatBotSupportMXBean { public final class ChatBotSupport implements ChatBotSupportMXBean {
private final String name; private final @NotNull ChatBotSupervisor supervisor;
private final BotConfigSupport config; private final @NotNull String name;
private final @NotNull BotConfigSupport config;
public static void register(String name, BotConfig config) { public static void register(@NotNull ChatBotSupervisor supervisor, @NotNull String name, @NotNull BotConfig config) {
try { try {
var impl = new ChatBotSupport(supervisor, name, config);
var server = ManagementFactory.getPlatformMBeanServer(); var server = ManagementFactory.getPlatformMBeanServer();
server.registerMBean(new AdvancedMBean(new ChatBotSupport(name, config)), ChatBotSupportMXBean.getObjectName(name)); server.registerMBean(new AdvancedMBean(impl), ChatBotSupportMXBean.getObjectName(name));
} catch (Exception ex) { } catch (Exception ex) {
log.error("Could not register bot as an MBean.", ex); log.error("Could not register bot as an MBean.", ex);
} }
} }
public static void unregister(String name) { public static void unregister(@NotNull String name) {
try { try {
var server = ManagementFactory.getPlatformMBeanServer(); var server = ManagementFactory.getPlatformMBeanServer();
server.unregisterMBean(ChatBotSupportMXBean.getObjectName(name)); server.unregisterMBean(ChatBotSupportMXBean.getObjectName(name));
@ -33,12 +37,13 @@ public class ChatBotSupport implements ChatBotSupportMXBean {
} }
} }
private ChatBotSupport(String name, BotConfig config) { private ChatBotSupport(@NotNull ChatBotSupervisor supervisor, @NotNull String name, @NotNull BotConfig config) {
this.name = name; this.supervisor = Objects.requireNonNull(supervisor);
this.name = Objects.requireNonNull(name);
this.config = new BotConfigSupport(config); this.config = new BotConfigSupport(config);
} }
public void stop() throws InterruptedException { public void stop() throws InterruptedException {
ChatBotSupervisor.INSTANCE.stop(name); supervisor.stop(name);
} }
} }

@ -2,43 +2,48 @@ package eu.jonahbauer.chat.server.management.impl;
import eu.jonahbauer.chat.server.management.SocketManagerMXBean; import eu.jonahbauer.chat.server.management.SocketManagerMXBean;
import eu.jonahbauer.chat.server.socket.SocketSupervisor; import eu.jonahbauer.chat.server.socket.SocketSupervisor;
import lombok.SneakyThrows;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import javax.management.JMException;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.util.Objects;
import java.util.SortedSet; import java.util.SortedSet;
public enum SocketManager implements SocketManagerMXBean { public final class SocketManager implements SocketManagerMXBean {
INSTANCE; private final @NotNull SocketSupervisor supervisor;
@SneakyThrows public static void init(@NotNull SocketSupervisor supervisor) throws JMException {
SocketManager() { var impl = new SocketManager(supervisor);
var server = ManagementFactory.getPlatformMBeanServer(); var server = ManagementFactory.getPlatformMBeanServer();
server.registerMBean(new AdvancedMBean(this), SocketManagerMXBean.NAME); server.registerMBean(new AdvancedMBean(impl), SocketManagerMXBean.NAME);
}
private SocketManager(@NotNull SocketSupervisor supervisor) {
this.supervisor = Objects.requireNonNull(supervisor);
} }
@Override @Override
public void setCredentials(@NotNull String username, @NotNull String password) { public void setCredentials(@NotNull String username, @NotNull String password) {
SocketSupervisor.INSTANCE.setAccount(username, password); supervisor.setAccount(username, password);
} }
@Override @Override
public void start(@NotNull String channel) { public void start(@NotNull String channel) {
SocketSupervisor.INSTANCE.start(channel); supervisor.start(channel);
} }
@Override @Override
public void stop(@NotNull String channel) throws InterruptedException { public void stop(@NotNull String channel) throws InterruptedException {
SocketSupervisor.INSTANCE.stop(channel); supervisor.stop(channel);
} }
@Override @Override
public void stop() throws InterruptedException { public void stop() throws InterruptedException {
SocketSupervisor.INSTANCE.stop(); supervisor.stop();
} }
@Override @Override
public @NotNull SortedSet<@NotNull String> getChannels() { public @NotNull SortedSet<@NotNull String> getChannels() {
return SocketSupervisor.INSTANCE.getChannels(); return supervisor.getChannels();
} }
} }

@ -3,30 +3,32 @@ package eu.jonahbauer.chat.server.management.impl;
import eu.jonahbauer.chat.server.management.SocketState; import eu.jonahbauer.chat.server.management.SocketState;
import eu.jonahbauer.chat.server.management.SocketSupportMXBean; import eu.jonahbauer.chat.server.management.SocketSupportMXBean;
import eu.jonahbauer.chat.server.socket.SocketSupervisor; import eu.jonahbauer.chat.server.socket.SocketSupervisor;
import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
import java.util.Date; import java.util.Date;
import java.util.Objects;
@Log4j2 @Log4j2
@Getter @Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE) public final class SocketSupport implements SocketSupportMXBean {
public class SocketSupport implements SocketSupportMXBean { private final @NotNull SocketSupervisor supervisor;
private final String channel; private final @NotNull String channel;
public static void register(String channel) { public static void register(@NotNull SocketSupervisor supervisor, @NotNull String channel) {
try { try {
var impl = new SocketSupport(supervisor, channel);
var server = ManagementFactory.getPlatformMBeanServer(); var server = ManagementFactory.getPlatformMBeanServer();
server.registerMBean(new AdvancedMBean(new SocketSupport(channel)), SocketSupportMXBean.getObjectName(channel)); server.registerMBean(new AdvancedMBean(impl), SocketSupportMXBean.getObjectName(channel));
} catch (Exception ex) { } catch (Exception ex) {
log.error("Could not register socket as an MBean.", ex); log.error("Could not register socket as an MBean.", ex);
} }
} }
public static void unregister(String name) { public static void unregister(@NotNull String name) {
try { try {
var server = ManagementFactory.getPlatformMBeanServer(); var server = ManagementFactory.getPlatformMBeanServer();
server.unregisterMBean(SocketSupportMXBean.getObjectName(name)); server.unregisterMBean(SocketSupportMXBean.getObjectName(name));
@ -35,30 +37,35 @@ public class SocketSupport implements SocketSupportMXBean {
} }
} }
private SocketSupport(@NotNull SocketSupervisor supervisor, @NotNull String channel) {
this.supervisor = Objects.requireNonNull(supervisor);
this.channel = Objects.requireNonNull(channel);
}
@Override @Override
public Date getCooldownUntil() { public @Nullable Date getCooldownUntil() {
var cooldown = SocketSupervisor.INSTANCE.getCooldownUntil(channel); var cooldown = supervisor.getCooldownUntil(channel);
return cooldown != null ? Date.from(cooldown) : null; return cooldown != null ? Date.from(cooldown) : null;
} }
@Override @Override
public SocketState getState() { public @Nullable SocketState getState() {
return SocketSupervisor.INSTANCE.getState(channel); return supervisor.getState(channel);
} }
@Override @Override
public void stop() throws InterruptedException { public void stop() throws InterruptedException {
SocketSupervisor.INSTANCE.stop(channel); supervisor.stop(channel);
} }
@Override @Override
public void restart() { public void restart() {
SocketSupervisor.INSTANCE.restart(channel); supervisor.restart(channel);
} }
@Override @Override
public void send(String name, String message, boolean bottag, boolean publicid) { public void send(@NotNull String name, @NotNull String message, boolean bottag, boolean publicid) {
SocketSupervisor.INSTANCE.send(channel, name, message, bottag, publicid); supervisor.send(channel, name, message, bottag, publicid);
} }
} }

@ -8,13 +8,16 @@ import eu.jonahbauer.chat.bot.api.Message;
import eu.jonahbauer.chat.server.Config; import eu.jonahbauer.chat.server.Config;
import eu.jonahbauer.chat.server.bot.ChatBotSupervisor; import eu.jonahbauer.chat.server.bot.ChatBotSupervisor;
import eu.jonahbauer.chat.server.management.SocketState; import eu.jonahbauer.chat.server.management.SocketState;
import eu.jonahbauer.chat.server.management.impl.SocketManager;
import eu.jonahbauer.chat.server.management.impl.SocketSupport; import eu.jonahbauer.chat.server.management.impl.SocketSupport;
import eu.jonahbauer.chat.server.util.Lazy;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Level;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import javax.management.JMException;
import java.io.IOException; import java.io.IOException;
import java.net.CookieManager; import java.net.CookieManager;
import java.net.URI; import java.net.URI;
@ -32,9 +35,7 @@ import java.util.concurrent.locks.ReentrantLock;
import static eu.jonahbauer.chat.server.util.UrlTemplateProcessor.URL; import static eu.jonahbauer.chat.server.util.UrlTemplateProcessor.URL;
@Log4j2 @Log4j2
public enum SocketSupervisor implements Chat { public final class SocketSupervisor implements Chat, AutoCloseable {
INSTANCE;
private static final URI AUTH_SERVER = URI.create("https://chat.qed-verein.de/rubychat/account"); private static final URI AUTH_SERVER = URI.create("https://chat.qed-verein.de/rubychat/account");
private static final String ORIGIN = "https://chat.qed-verein.de"; private static final String ORIGIN = "https://chat.qed-verein.de";
private static final String SERVER = "wss://chat.qed-verein.de/websocket?position=0&version=2&channel="; private static final String SERVER = "wss://chat.qed-verein.de/websocket?position=0&version=2&channel=";
@ -49,6 +50,13 @@ public enum SocketSupervisor implements Chat {
private final @NotNull CookieManager cookie = new CookieManager(); private final @NotNull CookieManager cookie = new CookieManager();
private final @NotNull HttpClient client = HttpClient.newBuilder().cookieHandler(cookie).build(); private final @NotNull HttpClient client = HttpClient.newBuilder().cookieHandler(cookie).build();
private final @NotNull ConcurrentMap<@NotNull String, @NotNull ChatClient> sockets = new ConcurrentHashMap<>(); private final @NotNull ConcurrentMap<@NotNull String, @NotNull ChatClient> sockets = new ConcurrentHashMap<>();
private final @NotNull Lazy<ChatBotSupervisor> chatBotSupervisor;
private final @NotNull ReentrantLock lock = new ReentrantLock();
public SocketSupervisor(@NotNull Lazy<ChatBotSupervisor> chatBotSupervisor) throws JMException {
SocketManager.init(this);
this.chatBotSupervisor = chatBotSupervisor;
}
public void setAccount(@NotNull Config.Account account) { public void setAccount(@NotNull Config.Account account) {
this.account = Objects.requireNonNull(account, "account"); this.account = Objects.requireNonNull(account, "account");
@ -82,11 +90,18 @@ public enum SocketSupervisor implements Chat {
* @param config a configuration * @param config a configuration
* @throws IllegalStateException if any sockets are already running * @throws IllegalStateException if any sockets are already running
*/ */
public synchronized void start(@NotNull Config config) { public void start(@NotNull Config config) {
if (!this.sockets.isEmpty()) throw new IllegalStateException("start(Config) may not be used when any sockets are running already"); lock.lock();
try {
if (!this.sockets.isEmpty()) {
throw new IllegalStateException("start(Config) may not be used when any sockets are running already");
}
setAccount(config.account()); setAccount(config.account());
config.channels().forEach(this::start); config.channels().forEach(this::start);
} finally {
lock.unlock();
}
} }
/** /**
@ -94,11 +109,18 @@ public enum SocketSupervisor implements Chat {
* @param channel the channel * @param channel the channel
* @throws IllegalStateException if a socket is already connected to that channel * @throws IllegalStateException if a socket is already connected to that channel
*/ */
public synchronized void start(@NotNull String channel) { public void start(@NotNull String channel) {
if (sockets.containsKey(channel)) throw new SocketCreationException("Duplicate channel: " + channel); lock.lock();
try {
if (sockets.containsKey(channel)) {
throw new SocketCreationException("Duplicate channel: " + channel);
}
var socket = new ChatClient(channel); var socket = new ChatClient(channel);
this.sockets.put(channel, socket); this.sockets.put(channel, socket);
} finally {
lock.unlock();
}
} }
/** /**
@ -151,6 +173,11 @@ public enum SocketSupervisor implements Chat {
} }
} }
@Override
public void close() throws InterruptedException {
stop();
}
public @NotNull SortedSet<@NotNull String> getChannels() { public @NotNull SortedSet<@NotNull String> getChannels() {
return Collections.unmodifiableSortedSet(new TreeSet<>(sockets.keySet())); return Collections.unmodifiableSortedSet(new TreeSet<>(sockets.keySet()));
} }
@ -191,7 +218,7 @@ public enum SocketSupervisor implements Chat {
return credentials; return credentials;
} }
private record Credentials(String userid, String pwhash) {} private record Credentials(@NotNull String userid, @NotNull String pwhash) {}
private sealed interface ChatClientState { private sealed interface ChatClientState {
@ -218,7 +245,7 @@ public enum SocketSupervisor implements Chat {
} }
private final class ChatClient implements WebSocket.Listener, ChatClientState { private final class ChatClient implements WebSocket.Listener, ChatClientState {
private final String channel; private final @NotNull String channel;
private final @NotNull ReentrantLock lock = new ReentrantLock(); private final @NotNull ReentrantLock lock = new ReentrantLock();
private final @NotNull CountDownLatch stopped = new CountDownLatch(1); private final @NotNull CountDownLatch stopped = new CountDownLatch(1);
@ -227,7 +254,7 @@ public enum SocketSupervisor implements Chat {
public ChatClient(@NotNull String channel) { public ChatClient(@NotNull String channel) {
this.channel = channel; this.channel = channel;
SocketSupport.register(channel); SocketSupport.register(SocketSupervisor.this, channel);
this.state.onEnter(); this.state.onEnter();
} }
@ -427,7 +454,7 @@ public enum SocketSupervisor implements Chat {
if (message instanceof Message.Post post) { if (message instanceof Message.Post post) {
delay = post.id() + 1; delay = post.id() + 1;
ChatBotSupervisor.INSTANCE.onMessage(post); SocketSupervisor.this.chatBotSupervisor.get().onMessage(post);
} }
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
log.warn("Could not parse as message: {}", text, e); log.warn("Could not parse as message: {}", text, e);

@ -0,0 +1,26 @@
package eu.jonahbauer.chat.server.util;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
public interface Lazy<T> {
@NotNull T get();
class MutableLazy<T> implements Lazy<T> {
private T value;
@Override
public @NotNull T get() {
var value = this.value;
if (value == null) throw new IllegalStateException();
return value;
}
public @NotNull T set(@NotNull T value) {
if (this.value != null) throw new IllegalStateException();
this.value = Objects.requireNonNull(value);
return value;
}
}
}
Loading…
Cancel
Save