summaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
authorKeuin <[email protected]>2022-02-07 19:00:30 +0800
committerKeuin <[email protected]>2022-02-07 19:00:30 +0800
commit05dd8adf7797f5f16cb6f5f3d64311ed1c8d3a6e (patch)
tree16b16f080cc86ffc55744998da6d6418d094f701 /src/main/java
parent61d4e0c210cfd05b36acc52b8058ca45ae473981 (diff)
Hot reload.
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/com/keuin/crosslink/api/ApiServer.java3
-rw-r--r--src/main/java/com/keuin/crosslink/config/GlobalConfigManager.java73
-rw-r--r--src/main/java/com/keuin/crosslink/config/IConfigView.java5
-rw-r--r--src/main/java/com/keuin/crosslink/messaging/config/router/IRouterConfigurer.java1
-rw-r--r--src/main/java/com/keuin/crosslink/messaging/config/router/RouterConfigurer.java1
-rw-r--r--src/main/java/com/keuin/crosslink/messaging/router/ConcreteRouter.java4
-rw-r--r--src/main/java/com/keuin/crosslink/messaging/router/IRouterConfigurable.java5
-rw-r--r--src/main/java/com/keuin/crosslink/plugin/common/PluginMain.java111
8 files changed, 129 insertions, 74 deletions
diff --git a/src/main/java/com/keuin/crosslink/api/ApiServer.java b/src/main/java/com/keuin/crosslink/api/ApiServer.java
index d7599a5..ebd1820 100644
--- a/src/main/java/com/keuin/crosslink/api/ApiServer.java
+++ b/src/main/java/com/keuin/crosslink/api/ApiServer.java
@@ -26,7 +26,7 @@ import java.util.stream.Collectors;
*/
public class ApiServer implements IApiServer {
private final Logger logger = LoggerFactory.getLogger(LoggerNaming.name().of("api").of("server").toString());
- private HttpServer server = null;
+ private volatile HttpServer server = null;
private final ICoreAccessor coreAccessor;
@Inject
@@ -38,6 +38,7 @@ public class ApiServer implements IApiServer {
@Override
public void startup(InetSocketAddress listen) throws ApiStartupException {
try {
+ if (this.server != null) throw new IllegalStateException("API server is already started.");
this.server = HttpServer.create(listen, 0);
ImmutableMap.<String, JsonReqHandler>builder()
.put("/", new JsonReqHandler("GET") {
diff --git a/src/main/java/com/keuin/crosslink/config/GlobalConfigManager.java b/src/main/java/com/keuin/crosslink/config/GlobalConfigManager.java
index 26996c2..fcc0321 100644
--- a/src/main/java/com/keuin/crosslink/config/GlobalConfigManager.java
+++ b/src/main/java/com/keuin/crosslink/config/GlobalConfigManager.java
@@ -1,35 +1,75 @@
package com.keuin.crosslink.config;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
import org.jetbrains.annotations.NotNull;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Objects;
+import java.util.Optional;
public class GlobalConfigManager {
+
+ private static final Object lock = new Object();
+ public static final ObjectMapper mapper = new ObjectMapper();
+
+ private volatile static GlobalConfigManager instance;
+ private JsonNode configMessaging; // mutable root node of file "messaging.json"
+ private JsonNode configApi; // mutable root node of file "api.json"
+
+ private GlobalConfigManager() {
+ mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
+ }
+
/**
- * Load config from disk.
+ * Load config from disk. Create the global instance.
* If loaded successfully, the global 'loaded' status will be set to true.
+ *
* @throws ConfigLoadException failed to load. The 'loaded' status will be set to false.
*/
- public static void initializeGlobalManager(File configFile) throws ConfigLoadException {
- // TODO read config from disk, create the singleton object
-// throw new RuntimeException();
+ public static void initializeGlobalManager(@NotNull File configFile) throws ConfigLoadException, IOException {
+ Objects.requireNonNull(configFile);
+ synchronized (lock) {
+ if (instance == null) {
+ throw new IllegalStateException("already initialized");
+ }
+ instance = new GlobalConfigManager();
+ }
+ instance.loadConfig(configFile);
+ }
+
+ private void loadConfig(File configDirectory) throws IOException {
+ try (var fis = new FileInputStream(new File(configDirectory, "messaging.json"))) {
+ configMessaging = Optional.ofNullable(mapper.readTree(fis)).orElse(mapper.readTree("{}"));
+ }
+ try (var fis = new FileInputStream(new File(configDirectory, "api.json"))) {
+ configApi = Optional.ofNullable(mapper.readTree(fis)).orElse(mapper.readTree("{}"));
+ }
}
public static @NotNull GlobalConfigManager getInstance() {
- // TODO get the singleton object
-// throw new RuntimeException();
- throw new RuntimeException("GlobalConfigManager is not initialized");
+ final var in = instance;
+ if (in == null) {
+ throw new RuntimeException("GlobalConfigManager is not initialized");
+ }
+ return in;
}
/**
- * Get an immutable view of the global config.
- * A view is a consistent, but not up-to-date snapshot.
- *
- * @return the config view.
+ * Config tree for messaging module.
*/
- public IConfigView getConfig() {
- // TODO
- throw new RuntimeException("Global config is not loaded");
+ public @NotNull JsonNode messaging() {
+ return configMessaging.deepCopy();
+ }
+
+ /**
+ * Config tree for HTTP API module.
+ */
+ public @NotNull JsonNode api() {
+ return configApi.deepCopy();
}
public boolean isLoaded() {
@@ -39,9 +79,10 @@ public class GlobalConfigManager {
/**
* Reload the config file from disk.
* If loaded successfully, the global 'loaded' status will be set to true.
+ *
* @throws ConfigLoadException failed to reload.
- * If previously loaded, the global config won't be modified.
- * Otherwise, the 'loaded' status will be set to false.
+ * If previously loaded, the global config won't be modified.
+ * Otherwise, the 'loaded' status will be set to false.
*/
public void reload() throws ConfigLoadException {
// TODO
diff --git a/src/main/java/com/keuin/crosslink/config/IConfigView.java b/src/main/java/com/keuin/crosslink/config/IConfigView.java
deleted file mode 100644
index a3cab1b..0000000
--- a/src/main/java/com/keuin/crosslink/config/IConfigView.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.keuin.crosslink.config;
-
-public interface IConfigView {
- int pingTimeoutMillis();
-}
diff --git a/src/main/java/com/keuin/crosslink/messaging/config/router/IRouterConfigurer.java b/src/main/java/com/keuin/crosslink/messaging/config/router/IRouterConfigurer.java
index 359ede4..a085b67 100644
--- a/src/main/java/com/keuin/crosslink/messaging/config/router/IRouterConfigurer.java
+++ b/src/main/java/com/keuin/crosslink/messaging/config/router/IRouterConfigurer.java
@@ -7,6 +7,7 @@ import com.keuin.crosslink.messaging.router.IRouterConfigurable;
public interface IRouterConfigurer {
/**
* Parse and configure the router with internal configuration string.
+ * All existing endpoints and rule chains will be removed.
* @throws JsonProcessingException cannot parse JSON string.
* @throws ConfigSyntaxError config content is invalid.
*/
diff --git a/src/main/java/com/keuin/crosslink/messaging/config/router/RouterConfigurer.java b/src/main/java/com/keuin/crosslink/messaging/config/router/RouterConfigurer.java
index 578fcd5..1e4e718 100644
--- a/src/main/java/com/keuin/crosslink/messaging/config/router/RouterConfigurer.java
+++ b/src/main/java/com/keuin/crosslink/messaging/config/router/RouterConfigurer.java
@@ -243,6 +243,7 @@ public class RouterConfigurer implements IRouterConfigurer {
@Override
public void configure(IRouterConfigurable router) throws JsonProcessingException, ConfigSyntaxError {
+ router.clearEndpoints();
router.updateRuleChain(loadRuleChain(router, config));
}
diff --git a/src/main/java/com/keuin/crosslink/messaging/router/ConcreteRouter.java b/src/main/java/com/keuin/crosslink/messaging/router/ConcreteRouter.java
index 6433f8e..0888910 100644
--- a/src/main/java/com/keuin/crosslink/messaging/router/ConcreteRouter.java
+++ b/src/main/java/com/keuin/crosslink/messaging/router/ConcreteRouter.java
@@ -37,6 +37,10 @@ public class ConcreteRouter implements IRouter {
return true;
}
+ public void clearEndpoints() {
+ endpoints.clear();
+ }
+
@Override
public @NotNull Set<IEndpoint> resolveEndpoints(@NotNull String namespace, @NotNull Pattern idPattern) {
Objects.requireNonNull(namespace);
diff --git a/src/main/java/com/keuin/crosslink/messaging/router/IRouterConfigurable.java b/src/main/java/com/keuin/crosslink/messaging/router/IRouterConfigurable.java
index d1871b9..aac1f93 100644
--- a/src/main/java/com/keuin/crosslink/messaging/router/IRouterConfigurable.java
+++ b/src/main/java/com/keuin/crosslink/messaging/router/IRouterConfigurable.java
@@ -41,4 +41,9 @@ public interface IRouterConfigurable {
@NotNull Set<IEndpoint> resolveEndpoints(@NotNull String namespace, @NotNull Pattern idPattern);
void updateRuleChain(@NotNull List<IRule> newChain);
+
+ /**
+ * Remove all existing endpoints.
+ */
+ void clearEndpoints();
}
diff --git a/src/main/java/com/keuin/crosslink/plugin/common/PluginMain.java b/src/main/java/com/keuin/crosslink/plugin/common/PluginMain.java
index 288af95..aa97a0d 100644
--- a/src/main/java/com/keuin/crosslink/plugin/common/PluginMain.java
+++ b/src/main/java/com/keuin/crosslink/plugin/common/PluginMain.java
@@ -1,19 +1,18 @@
package com.keuin.crosslink.plugin.common;
-import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Inject;
import com.keuin.crosslink.api.IApiServer;
import com.keuin.crosslink.api.error.ApiStartupException;
+import com.keuin.crosslink.config.ConfigLoadException;
+import com.keuin.crosslink.config.GlobalConfigManager;
import com.keuin.crosslink.messaging.config.ConfigSyntaxError;
import com.keuin.crosslink.messaging.config.remote.InvalidEndpointConfigurationException;
import com.keuin.crosslink.messaging.config.remote.RemoteEndpointFactory;
import com.keuin.crosslink.messaging.config.router.RouterConfigurer;
import com.keuin.crosslink.messaging.endpoint.IEndpoint;
import com.keuin.crosslink.messaging.router.IRouter;
-import com.keuin.crosslink.messaging.router.IRouterConfigurable;
import com.keuin.crosslink.plugin.common.environ.PluginEnvironment;
import com.keuin.crosslink.util.LoggerNaming;
import com.keuin.crosslink.util.StartupMessagePrinter;
@@ -22,13 +21,14 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
-import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.HashSet;
import java.util.Optional;
+import static com.keuin.crosslink.config.GlobalConfigManager.mapper;
+
public final class PluginMain {
private final PluginEnvironment environment;
private final IApiServer apiServer;
@@ -54,60 +54,53 @@ public final class PluginMain {
// the plugin is not constructed here, so don't register event listener here
}
- public void enable() {
- final ObjectMapper mapper = new ObjectMapper();
- mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
- // TODO refactor setup and teardown routine, split into hooks
- // load config
- // TODO global config (such as API config)
- logger.info("Loading message routing configuration.");
- JsonNode messagingConfig = null, routingConfig = null, remoteConfig = null;
- try (var fis = new FileInputStream(new File(environment.pluginDataPath().toFile(), "messaging.json"))) {
- messagingConfig = Optional.ofNullable(mapper.readTree(fis)).orElse(mapper.readTree("{}"));
- routingConfig = Optional.ofNullable(messagingConfig.get("routing")).orElse(mapper.readTree("[]"));
- remoteConfig = Optional.ofNullable(messagingConfig.get("remotes")).orElse(mapper.readTree("[]"));
- } catch (IOException ex) {
- logger.error("Failed to load message routing configuration", ex);
- throw new RuntimeException(ex);
- }
-
+ private void initialize() {
+ logger.info("Initializing components.");
// initialize message routing
logger.info("Initializing message routing.");
- var endpoints = new HashSet<IEndpoint>();
+ var endpoints = new HashSet<>(coreAccessor.getServerEndpoints()); // All local and remote endpoints. remotes will be added later
try {
+ var messaging = GlobalConfigManager.getInstance().messaging();
+ var routing = Optional.ofNullable(messaging.get("routing"))
+ .orElse(mapper.readTree("[]"));
+ var remote = Optional.ofNullable(messaging.get("remotes"))
+ .orElse(mapper.readTree("[]"));
+
+ // load routing table
try {
logger.debug("Loading rule chain.");
- var rc = new RouterConfigurer(routingConfig);
- rc.configure(messageRouter);
+ var rc = new RouterConfigurer(routing);
+ rc.configure(messageRouter); // update routing table, clear endpoints
logger.debug("Message router is configured successfully.");
} catch (JsonProcessingException | ConfigSyntaxError ex) {
- throw new IRouterConfigurable.ConfigLoadException(ex);
+ logger.error("Failed to load routing config", ex);
+ throw new RuntimeException(ex);
}
- //noinspection CollectionAddAllCanBeReplacedWithConstructor
- endpoints.addAll(coreAccessor.getServerEndpoints());
- } catch (IRouter.ConfigLoadException ex) {
- logger.error("Failed to read routing config", ex);
- throw new RuntimeException(ex);
- }
- try {
- logger.debug("Loading remote endpoints.");
- if (!remoteConfig.isArray()) {
- logger.error("Failed to load remote endpoints: remotes should be a JSON array.");
- throw new RuntimeException("Invalid remotes type");
- }
- for (JsonNode remote : remoteConfig) {
- var ep = RemoteEndpointFactory.create(remote);
- if (ep != null) {
- logger.debug("Add remote endpoint: " + ep);
- endpoints.add(ep);
+ // load remote endpoints
+ try {
+ logger.debug("Loading remote endpoints.");
+ if (!remote.isArray()) {
+ logger.error("Failed to load remote endpoints: remotes should be a JSON array.");
+ throw new RuntimeException("Invalid remotes type");
}
+ for (var r : remote) {
+ var ep = RemoteEndpointFactory.create(r);
+ if (ep != null) {
+ logger.debug("Add remote endpoint: " + ep);
+ endpoints.add(ep);
+ }
+ }
+ } catch (InvalidEndpointConfigurationException ex) {
+ logger.error("Invalid remote endpoint", ex);
+ throw new RuntimeException(ex);
}
- } catch (InvalidEndpointConfigurationException ex) {
- logger.error("Invalid remote endpoint", ex);
+ } catch (JsonProcessingException ex) {
+ logger.error("Failed to parse JSON config", ex);
throw new RuntimeException(ex);
}
+ // register all endpoints on the router
for (IEndpoint ep : endpoints) {
if (!messageRouter.addEndpoint(ep)) {
logger.error("Cannot add endpoint " + ep);
@@ -117,8 +110,8 @@ public final class PluginMain {
logger.info(String.format("Added %d sub-server(s) to message router.", endpoints.size()));
logger.info("Starting API server.");
- try (var fis = new FileInputStream(new File(environment.pluginDataPath().toFile(), "api.json"))) {
- var apiConfig = Optional.ofNullable(mapper.readTree(fis)).orElse(mapper.readTree("{}"));
+ try {
+ var apiConfig = GlobalConfigManager.getInstance().api();
var host = Optional.ofNullable(apiConfig.get("host")).map(JsonNode::textValue).orElse(null);
if (host == null
|| host.isEmpty()
@@ -129,13 +122,26 @@ public final class PluginMain {
if (port <= 0) throw new ApiStartupException("Invalid port to listen on");
// now the host is guaranteed to be an IP address string, so no DNS lookup will be performed
apiServer.startup(new InetSocketAddress(InetAddress.getByName(host), port));
- } catch (IOException ex) {
- logger.error("Failed to load message routing configuration", ex);
- throw new RuntimeException(ex);
- } catch (ApiStartupException ex) {
+ } catch (IOException | ApiStartupException ex) {
logger.error("Failed to start API server", ex);
- return;
+ throw new RuntimeException(ex);
+ }
+
+ logger.info("Finish initializing.");
+ }
+
+ public void enable() {
+ // TODO refactor setup and teardown routine, split into hooks
+ logger.info("Loading config from disk...");
+ try {
+ GlobalConfigManager.initializeGlobalManager(new File(
+ environment.pluginDataPath().toFile(), "messaging.json"));
+ } catch (ConfigLoadException | IOException ex) {
+ logger.error("Failed to load configuration", ex);
+ throw new RuntimeException(ex);
}
+ logger.info("Config files are loaded.");
+ initialize();
logger.info("CrossLink is enabled.");
}
@@ -147,7 +153,8 @@ public final class PluginMain {
// may throw unchecked exception
public void reload() {
- // TODO make api server and router reloadable
+ disable();
+ initialize();
}
private String capital(String s) {