initial commit
This commit is contained in:
15
management/build.gradle.kts
Normal file
15
management/build.gradle.kts
Normal file
@@ -0,0 +1,15 @@
|
||||
plugins {
|
||||
id("chat-bot.java-conventions")
|
||||
id("java-library")
|
||||
}
|
||||
|
||||
group = "eu.jonahbauer.chat"
|
||||
version = "0.1.0-SNAPSHOT"
|
||||
|
||||
dependencies {
|
||||
api(project(":bot-api")) {
|
||||
capabilities {
|
||||
requireCapability("eu.jonahbauer.chat:bot-api-config")
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,133 @@
|
||||
package eu.jonahbauer.chat.server.management;
|
||||
|
||||
import eu.jonahbauer.chat.bot.config.BotConfig;
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.management.openmbean.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Makes {@link BotConfig} accessible as JMX open data.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@EqualsAndHashCode
|
||||
public class BotConfigSupport implements CompositeDataView {
|
||||
private static final OpenType<String[]> STRING_ARRAY_TYPE;
|
||||
|
||||
static {
|
||||
try {
|
||||
STRING_ARRAY_TYPE = ArrayType.getArrayType(SimpleType.STRING);
|
||||
} catch (OpenDataException e) {
|
||||
throw Lombok.sneakyThrow(e);
|
||||
}
|
||||
}
|
||||
|
||||
private final @NotNull BotConfig config;
|
||||
|
||||
public BotConfigSupport(@NotNull BotConfig config) {
|
||||
this.config = Objects.requireNonNull(config);
|
||||
}
|
||||
|
||||
public @NotNull BotConfig unwrap() {
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
// at least one getter is required for this class to be a valid open data type
|
||||
public String getType() {
|
||||
return config.getType();
|
||||
}
|
||||
|
||||
// detected by java.management
|
||||
@SneakyThrows
|
||||
public static @NotNull BotConfigSupport from(@NotNull CompositeData composite) {
|
||||
var type = composite.getCompositeType();
|
||||
var keys = type.keySet();
|
||||
|
||||
var out = BotConfig.builder();
|
||||
for (var key : keys) {
|
||||
var fieldType = type.getType(key);
|
||||
var value = composite.get(key);
|
||||
|
||||
if (SimpleType.LONG.equals(fieldType)) {
|
||||
out.value(key, (Long) value);
|
||||
} else if (SimpleType.INTEGER.equals(fieldType)) {
|
||||
out.value(key, (Integer) value);
|
||||
} else if (SimpleType.SHORT.equals(fieldType)) {
|
||||
out.value(key, (Short) value);
|
||||
} else if (SimpleType.CHARACTER.equals(fieldType)) {
|
||||
out.value(key, (Character) value);
|
||||
} else if (SimpleType.BYTE.equals(fieldType)) {
|
||||
out.value(key, (Byte) value);
|
||||
} else if (SimpleType.DOUBLE.equals(fieldType)) {
|
||||
out.value(key, (Double) value);
|
||||
} else if (SimpleType.FLOAT.equals(fieldType)) {
|
||||
out.value(key, (Float) value);
|
||||
} else if (SimpleType.BOOLEAN.equals(fieldType)) {
|
||||
out.value(key, (Boolean) value);
|
||||
} else if (SimpleType.STRING.equals(fieldType)) {
|
||||
out.value(key, (String) value);
|
||||
} else if (STRING_ARRAY_TYPE.equals(fieldType)) {
|
||||
out.value(key, List.of((String[]) composite.get(key)));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported field type: " + fieldType.getDescription());
|
||||
}
|
||||
}
|
||||
return new BotConfigSupport(out.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull CompositeData toCompositeData(@NotNull CompositeType ct) {
|
||||
try {
|
||||
var itemNames = new ArrayList<String>();
|
||||
var itemTypes = new ArrayList<OpenType<?>>();
|
||||
var values = new ArrayList<>();
|
||||
|
||||
var keys = config.keySet();
|
||||
for (var key : keys) {
|
||||
var value = getOpenData(config.get(key).orElseThrow());
|
||||
var type = getOpenType(value);
|
||||
|
||||
itemNames.add(key);
|
||||
itemTypes.add(type);
|
||||
values.add(value);
|
||||
}
|
||||
|
||||
var xct = new CompositeType(
|
||||
ct.getTypeName(),
|
||||
ct.getDescription(),
|
||||
itemNames.toArray(new String[0]),
|
||||
itemNames.toArray(new String[0]),
|
||||
itemTypes.toArray(new OpenType<?>[0])
|
||||
);
|
||||
|
||||
return new CompositeDataSupport(xct, itemNames.toArray(new String[0]), values.toArray());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static @NotNull OpenType<?> getOpenType(@NotNull Object value) {
|
||||
return switch (value) {
|
||||
case Long _ -> SimpleType.LONG;
|
||||
case Double _ -> SimpleType.DOUBLE;
|
||||
case Boolean _ -> SimpleType.BOOLEAN;
|
||||
case String _ -> SimpleType.STRING;
|
||||
case String[] _ -> STRING_ARRAY_TYPE;
|
||||
default -> throw new IllegalArgumentException();
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousToArrayCall")
|
||||
private static Object getOpenData(Object value) {
|
||||
return value instanceof List<?> list ? list.toArray(new String[0]) : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return config.toString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
package eu.jonahbauer.chat.server.management;
|
||||
|
||||
import eu.jonahbauer.chat.server.management.annotations.ManagedAttribute;
|
||||
import eu.jonahbauer.chat.server.management.annotations.ManagedOperation;
|
||||
import eu.jonahbauer.chat.server.management.annotations.ManagedParameter;
|
||||
import lombok.SneakyThrows;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.management.MXBean;
|
||||
import javax.management.ObjectName;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.SortedSet;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@MXBean
|
||||
public interface ChatBotManagerMXBean {
|
||||
@NotNull ObjectName NAME = getObjectName();
|
||||
|
||||
@SneakyThrows
|
||||
private static @NotNull ObjectName getObjectName() {
|
||||
return ObjectName.getInstance("eu.jonahbauer.chat.bot.server", "component", "ChatBots");
|
||||
}
|
||||
|
||||
@ManagedOperation(description = "starts a bot with the given name and type", impact = ManagedOperation.Impact.ACTION)
|
||||
default void start(
|
||||
@ManagedParameter(name = "name", description = "the name of the bot (must be unique)") @NotNull String name,
|
||||
@ManagedParameter(name = "type", description = "the fully qualified class name of the bot") @NotNull String type,
|
||||
@ManagedParameter(name = "channel", description = "the channel the bot will be active in") @NotNull String channel
|
||||
) {
|
||||
start(name, type, List.of(channel));
|
||||
}
|
||||
|
||||
@ManagedOperation(description = "starts a bot with the given name and type", impact = ManagedOperation.Impact.ACTION)
|
||||
void start(
|
||||
@ManagedParameter(name = "name", description = "the name of the bot (must be unique)") @NotNull String name,
|
||||
@ManagedParameter(name = "type", description = "the fully qualified class name of the bot") @NotNull String type,
|
||||
@ManagedParameter(name = "channels", description = "the channels the bot will be active in") @NotNull List<@NotNull String> channels
|
||||
);
|
||||
|
||||
@ManagedOperation(description = "starts a bot with the given config under the specified name", impact = ManagedOperation.Impact.ACTION)
|
||||
void start(
|
||||
@ManagedParameter(name = "name", description = "the name of the bot (must be unique)") @NotNull String name,
|
||||
@ManagedParameter(name = "config", description = "the bot configuration") @NotNull BotConfigSupport config
|
||||
);
|
||||
|
||||
@ManagedOperation(description = "stops the bot with the given name and waits for it to finish", impact = ManagedOperation.Impact.ACTION)
|
||||
void stop(@ManagedParameter(name = "name", description = "the name of the bot") @NotNull String name) throws InterruptedException;
|
||||
|
||||
@ManagedOperation(description = "stops all currently running bots and waits for them to finish", impact = ManagedOperation.Impact.ACTION)
|
||||
void stop() throws InterruptedException;
|
||||
|
||||
@ManagedAttribute(description = "the list of all currently running bots.")
|
||||
@NotNull Map<@NotNull String, @NotNull BotConfigSupport> getBots();
|
||||
|
||||
@ManagedAttribute(description = "the list of all known bot implementations")
|
||||
@NotNull SortedSet<@NotNull String> getBotImplementations();
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package eu.jonahbauer.chat.server.management;
|
||||
|
||||
import eu.jonahbauer.chat.server.management.annotations.ManagedAttribute;
|
||||
import eu.jonahbauer.chat.server.management.annotations.ManagedOperation;
|
||||
import lombok.SneakyThrows;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.management.MXBean;
|
||||
import javax.management.ObjectName;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@MXBean
|
||||
public interface ChatBotSupportMXBean {
|
||||
|
||||
@SneakyThrows
|
||||
static @NotNull ObjectName getObjectName(String name) {
|
||||
return ObjectName.getInstance(STR."eu.jonahbauer.chat.bot.server:component=ChatBots,name=\{quote(name)}");
|
||||
}
|
||||
|
||||
private static @NotNull String quote(@NotNull String name) {
|
||||
var needsQuoting = name.chars().anyMatch(chr -> chr == ',' || chr == '=' || chr == ':' || chr == '"' || chr == '*' || chr == '?');
|
||||
return needsQuoting ? ObjectName.quote(name) : name;
|
||||
}
|
||||
|
||||
@ManagedAttribute(description = "the bot's name")
|
||||
@NotNull String getName();
|
||||
|
||||
@ManagedAttribute(description = "the bot's type")
|
||||
default @NotNull String getType() {
|
||||
return getConfig().getType();
|
||||
}
|
||||
|
||||
@ManagedAttribute(description = "the channels the bot is active in")
|
||||
default @NotNull List<@NotNull String> getChannels() {
|
||||
return getConfig().unwrap().getChannels();
|
||||
}
|
||||
|
||||
@ManagedAttribute(description = "whether the bot sets the bottag for its messages")
|
||||
default boolean isBotTag() {
|
||||
return getConfig().unwrap().isBotTag();
|
||||
}
|
||||
|
||||
@ManagedAttribute(description = "whether the bot reacts to messages from other bots")
|
||||
default boolean isIgnoreBots() {
|
||||
return getConfig().unwrap().isIgnoreBots();
|
||||
}
|
||||
|
||||
@ManagedAttribute(description = "whether the bot sends messages with public id")
|
||||
default boolean isPublicId() {
|
||||
return getConfig().unwrap().isPublicId();
|
||||
}
|
||||
|
||||
@ManagedAttribute(description = "the bot's config")
|
||||
@NotNull BotConfigSupport getConfig();
|
||||
|
||||
@ManagedOperation(description = "stops this bot", impact = ManagedOperation.Impact.ACTION)
|
||||
void stop() throws InterruptedException;
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
package eu.jonahbauer.chat.server.management;
|
||||
|
||||
import eu.jonahbauer.chat.server.management.annotations.ManagedAttribute;
|
||||
import eu.jonahbauer.chat.server.management.annotations.ManagedOperation;
|
||||
import eu.jonahbauer.chat.server.management.annotations.ManagedOperation.Impact;
|
||||
import eu.jonahbauer.chat.server.management.annotations.ManagedParameter;
|
||||
import lombok.SneakyThrows;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.management.MXBean;
|
||||
import javax.management.ObjectName;
|
||||
import java.util.SortedSet;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@MXBean
|
||||
public interface SocketManagerMXBean {
|
||||
@NotNull ObjectName NAME = getObjectName();
|
||||
|
||||
@SneakyThrows
|
||||
private static @NotNull ObjectName getObjectName() {
|
||||
return ObjectName.getInstance("eu.jonahbauer.chat.bot.server", "component", "Sockets");
|
||||
}
|
||||
|
||||
@ManagedOperation(description = "updates the credentials used to authenticate to the chat", impact = Impact.ACTION)
|
||||
void setCredentials(
|
||||
@ManagedParameter(name = "username") @NotNull String username,
|
||||
@ManagedParameter(name = "password") @NotNull String password
|
||||
);
|
||||
|
||||
@ManagedOperation(description = "starts a socket for the given channel", impact = Impact.ACTION)
|
||||
void start(@ManagedParameter(name = "channel") @NotNull String channel);
|
||||
|
||||
@ManagedOperation(description = "stops the socket for the given channel and waits for it to finish", impact = Impact.ACTION)
|
||||
void stop(@ManagedParameter(name = "channel") @NotNull String channel) throws InterruptedException;
|
||||
|
||||
@ManagedOperation(description = "stops all sockets and waits for them to finish", impact = Impact.ACTION)
|
||||
void stop() throws InterruptedException;
|
||||
|
||||
@ManagedAttribute(description = "the list of currently connected channels")
|
||||
@NotNull SortedSet<@NotNull String> getChannels();
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
package eu.jonahbauer.chat.server.management;
|
||||
|
||||
public enum SocketState {
|
||||
CREATED, CONNECTING, CONNECTED, COOLDOWN, STOPPING, STOPPED
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package eu.jonahbauer.chat.server.management;
|
||||
|
||||
import eu.jonahbauer.chat.server.management.annotations.ManagedAttribute;
|
||||
import eu.jonahbauer.chat.server.management.annotations.ManagedOperation;
|
||||
import eu.jonahbauer.chat.server.management.annotations.ManagedParameter;
|
||||
import lombok.SneakyThrows;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.management.MXBean;
|
||||
import javax.management.ObjectName;
|
||||
import java.util.Date;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@MXBean
|
||||
public interface SocketSupportMXBean {
|
||||
|
||||
@SneakyThrows
|
||||
static @NotNull ObjectName getObjectName(@NotNull String channel) {
|
||||
return ObjectName.getInstance(STR."eu.jonahbauer.chat.bot.server:component=Sockets,name=\{quote(channel)}");
|
||||
}
|
||||
|
||||
private static @NotNull String quote(@NotNull String channel) {
|
||||
var needsQuoting = channel.chars().anyMatch(chr -> chr == ',' || chr == '=' || chr == ':' || chr == '"' || chr == '*' || chr == '?');
|
||||
return needsQuoting ? ObjectName.quote(channel) : channel;
|
||||
}
|
||||
|
||||
@ManagedAttribute(description = "the socket's state")
|
||||
SocketState getState();
|
||||
|
||||
@ManagedAttribute(description = "the channel this socket is connected to")
|
||||
String getChannel();
|
||||
|
||||
@ManagedAttribute(description = "the time when the current cooldown will end")
|
||||
Date getCooldownUntil();
|
||||
|
||||
|
||||
@ManagedOperation(description = "stops this socket", impact = ManagedOperation.Impact.ACTION)
|
||||
void stop() throws InterruptedException;
|
||||
|
||||
@ManagedOperation(description = "forcefully restarts the socket when its currently on cooldown", impact = ManagedOperation.Impact.ACTION)
|
||||
void restart();
|
||||
|
||||
@ManagedOperation(description = "sends a message via this socket", impact = ManagedOperation.Impact.ACTION)
|
||||
default void send(
|
||||
@ManagedParameter(name = "name") String name,
|
||||
@ManagedParameter(name = "message") String message
|
||||
) {
|
||||
send(name, message, true, true);
|
||||
}
|
||||
|
||||
@ManagedOperation(description = "sends a message via this socket", impact = ManagedOperation.Impact.ACTION)
|
||||
void send(
|
||||
@ManagedParameter(name = "name") String name,
|
||||
@ManagedParameter(name = "message") String message,
|
||||
@ManagedParameter(name = "bottag") boolean bottag,
|
||||
@ManagedParameter(name = "publicic") boolean publicid
|
||||
);
|
||||
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package eu.jonahbauer.chat.server.management.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface ManagedAttribute {
|
||||
String description() default "";
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package eu.jonahbauer.chat.server.management.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface ManagedBean {
|
||||
String description();
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package eu.jonahbauer.chat.server.management.annotations;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import javax.management.MBeanOperationInfo;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface ManagedOperation {
|
||||
String description() default "";
|
||||
Impact impact() default Impact.UNKNOWN;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
enum Impact {
|
||||
INFO(MBeanOperationInfo.INFO),
|
||||
ACTION(MBeanOperationInfo.ACTION),
|
||||
ACTION_INFO(MBeanOperationInfo.ACTION_INFO),
|
||||
UNKNOWN(MBeanOperationInfo.UNKNOWN);
|
||||
|
||||
private final int code;
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package eu.jonahbauer.chat.server.management.annotations;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.PARAMETER)
|
||||
public @interface ManagedParameter {
|
||||
String name() default "";
|
||||
String description() default "";
|
||||
}
|
10
management/src/main/java/module-info.java
Normal file
10
management/src/main/java/module-info.java
Normal file
@@ -0,0 +1,10 @@
|
||||
module eu.jonahbauer.chat.server.management {
|
||||
exports eu.jonahbauer.chat.server.management;
|
||||
exports eu.jonahbauer.chat.server.management.annotations;
|
||||
|
||||
requires transitive java.management;
|
||||
requires transitive eu.jonahbauer.chat.bot.config;
|
||||
requires static transitive org.jetbrains.annotations;
|
||||
|
||||
requires static lombok;
|
||||
}
|
Reference in New Issue
Block a user