From e48fdf37c7e3881142adcee08450de9ee3ec590b Mon Sep 17 00:00:00 2001 From: Kishalay Pandey Date: Sat, 22 Feb 2025 22:48:17 +0530 Subject: [PATCH 01/11] #1086: Init commit adding module --- microkernel-architecture/README.md | 27 +++++++ microkernel-architecture/pom.xml | 72 +++++++++++++++++++ .../java/com/iluwatar/microkernel/App.java | 33 +++++++++ .../microkernel/core/CoreService.java | 6 ++ .../iluwatar/microkernel/plugins/Plugin.java | 6 ++ .../microkernel/registry/PluginRegistry.java | 6 ++ .../com/iluwatar/microkernel/AppTest.java | 33 +++++++++ 7 files changed, 183 insertions(+) create mode 100644 microkernel-architecture/README.md create mode 100644 microkernel-architecture/pom.xml create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/App.java create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/CoreService.java create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/Plugin.java create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/registry/PluginRegistry.java create mode 100644 microkernel-architecture/src/test/java/com/iluwatar/microkernel/AppTest.java diff --git a/microkernel-architecture/README.md b/microkernel-architecture/README.md new file mode 100644 index 000000000000..3c3382a701f5 --- /dev/null +++ b/microkernel-architecture/README.md @@ -0,0 +1,27 @@ +--- +title: "Microkernel architecture Pattern in Java: Extending applications through plugins" +shortTitle: Microkernel architecture +description: "The Microkernel Architecture design pattern, also known as the plug-in architecture, is ideal for product-based applications that need to extend their core functionalities through plug-ins." +category: Data access +language: en +tag: + - Data access + - Data processing + - Persistence +--- + +## Also known as + +## Intent of Microkernel architecture Design Pattern + +## Detailed Explanation of Microkernel architecture Pattern with Real-World Examples + +## When to Use the Microkernel architecture Pattern in Java + +## Real-World Applications of Microkernel architecture Pattern in Java + +## Benefits and Trade-offs of Microkernel architecture Pattern + +## Related Java Design Patterns + +## References and Credits diff --git a/microkernel-architecture/pom.xml b/microkernel-architecture/pom.xml new file mode 100644 index 000000000000..c379b6a41991 --- /dev/null +++ b/microkernel-architecture/pom.xml @@ -0,0 +1,72 @@ + + + + + microkernel-architecture + 4.0.0 + + java-design-patterns + com.iluwatar + 1.26.0-SNAPSHOT + + + + + + maven-assembly-plugin + + + + + + com.iluwatar.microkernel.App + + + + + + org.apache.maven.plugins + + + + + + + junit-jupiter-engine + org.junit.jupiter + test + + + h2 + com.h2database + + + + diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/App.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/App.java new file mode 100644 index 000000000000..60c71d1681ff --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/App.java @@ -0,0 +1,33 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel; + +public class App { + + public static void main(String[] args) { + //TODO: Execute Core Application + } + +} diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/CoreService.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/CoreService.java new file mode 100644 index 000000000000..5887281910f1 --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/CoreService.java @@ -0,0 +1,6 @@ +package com.iluwatar.microkernel.core; + +public class CoreService { + + //TODO: Implement Core Services +} diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/Plugin.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/Plugin.java new file mode 100644 index 000000000000..c80ccb4f8f62 --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/Plugin.java @@ -0,0 +1,6 @@ +package com.iluwatar.microkernel.plugins; + +public interface Plugin { + + //TODO: Add common functions +} diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/registry/PluginRegistry.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/registry/PluginRegistry.java new file mode 100644 index 000000000000..1fe4004ea815 --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/registry/PluginRegistry.java @@ -0,0 +1,6 @@ +package com.iluwatar.microkernel.registry; + +public class PluginRegistry { + + //TODO: Implement Registry +} diff --git a/microkernel-architecture/src/test/java/com/iluwatar/microkernel/AppTest.java b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/AppTest.java new file mode 100644 index 000000000000..873d71fdf343 --- /dev/null +++ b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/AppTest.java @@ -0,0 +1,33 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class AppTest { + + //TODO: Add Tests +} From d03adead42334b0e164cddd82b68606b85e40b38 Mon Sep 17 00:00:00 2001 From: KishalayP Date: Mon, 28 Sep 2026 00:40:21 +0530 Subject: [PATCH 02/11] Adding Core Functions --- .../microkernel/core/MicroKernel.java | 117 ++++++++++++++++++ .../iluwatar/microkernel/ipc/IpcRouter.java | 82 ++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/MicroKernel.java create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/ipc/IpcRouter.java diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/MicroKernel.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/MicroKernel.java new file mode 100644 index 000000000000..bed4ad289741 --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/MicroKernel.java @@ -0,0 +1,117 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.core; + +import com.iluwatar.microkernel.ipc.IpcRouter; +import com.iluwatar.microkernel.ipc.Message; +import com.iluwatar.microkernel.registry.PluginRegistry; +import java.util.List; +import lombok.extern.slf4j.Slf4j; + +/** Manages plugin lifecycle, document state, and plugin-directed transformations. */ +@Slf4j +public class MicroKernel { + + private final PluginRegistry registry; + private final IpcRouter ipcRouter; + + // The kernel now owns the core domain state + private final StringBuilder documentBuffer; + + /** Creates an empty kernel with its own plugin registry and message router. */ + public MicroKernel() { + this.registry = new PluginRegistry(); + this.ipcRouter = new IpcRouter(registry); + this.documentBuffer = new StringBuilder(); + } + + /** Registers a plugin and gives it access to the kernel's message router. */ + public void loadPlugin(Plugin plugin) { + registry.register(plugin); + plugin.initialize(ipcRouter); + } + + /** Starts a registered plugin, if present. */ + public void startPlugin(String pluginName) { + var plugin = registry.getPlugin(pluginName); + if (plugin != null) { + plugin.onStart(); + } + } + + /** Stops and removes a registered plugin, if present. */ + public void unloadPlugin(String pluginName) { + var plugin = registry.getPlugin(pluginName); + if (plugin != null) { + plugin.onStop(); + registry.deregister(pluginName); + } + } + + /** Returns the names of all registered plugins. */ + public List getActivePlugins() { + return registry.getRegisteredPluginNames(); + } + + /** Appends a line of text to the document. */ + public void addTextToDocument(String text) { + documentBuffer.append(text).append("\n"); + } + + /** Returns the current document contents. */ + public String readDocument() { + return documentBuffer.toString(); + } + + /** Removes all text from the document. */ + public void clearDocument() { + documentBuffer.setLength(0); + } + + /** + * Sends the document to a registered plugin and replaces it with a successful transformation. + * + * @param pluginName the registered plugin to invoke + * @return a status message describing the outcome + */ + public String transformDocumentWithPlugin(String pluginName) { + if (!registry.getRegisteredPluginNames().contains(pluginName)) { + return "ERROR: Plugin '" + pluginName + "' is not active"; + } + + // Kernel sends its own document state to the plugin + Message msg = new Message("Kernel", pluginName, "TRANSFORM", documentBuffer.toString()); + String transformedText = ipcRouter.sendMessage(msg); + + if (transformedText.startsWith("ERROR") || transformedText.startsWith("IGNORED")) { + return "Transformation failed: " + transformedText; + } + + // Kernel updates its internal state based on the plugin's response + clearDocument(); + documentBuffer.append(transformedText); + return "Success: Document transformed by " + pluginName; + } +} diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/ipc/IpcRouter.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/ipc/IpcRouter.java new file mode 100644 index 000000000000..f1e4a985657e --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/ipc/IpcRouter.java @@ -0,0 +1,82 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.ipc; + +import com.iluwatar.microkernel.registry.PluginRegistry; +import lombok.extern.slf4j.Slf4j; + +/** Routes messages to registered, started plugins and isolates plugin failures. */ +@Slf4j +public class IpcRouter { + + private final PluginRegistry registry; + + /** + * Creates a router backed by the given plugin registry. + * + * @param registry the source of registered plugins + */ + public IpcRouter(PluginRegistry registry) { + this.registry = registry; + } + + /** + * Delivers a message to its registered, started recipient. + * + * @param message the message to route + * @return the recipient's response or an error message when delivery fails + */ + public String sendMessage(Message message) { + LOGGER.info( + "IPC: Routing message from [{}] to [{}] - Action: {}", + message.sender(), + message.recipient(), + message.action()); + + var targetPlugin = registry.getPlugin(message.recipient()); + + if (targetPlugin == null) { + LOGGER.warn("IPC Error: Recipient [{}] not found in registry.", message.recipient()); + return "ERROR: Destination Unreachable"; + } + + if (!targetPlugin.isStarted()) { + LOGGER.warn( + "IPC Error: Plugin [{}] is stopped and cannot process messages.", message.recipient()); + return "ERROR: Plugin Stopped"; + } + + // FAULT ISOLATION: A crash here will not crash the main application + try { + return targetPlugin.handleMessage(message); + } catch (Exception e) { + LOGGER.error( + "IPC Severe: Plugin [{}] crashed during execution! Isolating fault.", + targetPlugin.getName(), + e); + return "ERROR: Plugin Fault Isolated"; + } + } +} From 812b14fae494eaf4f2c0deee387444e140e4ea31 Mon Sep 17 00:00:00 2001 From: KishalayP Date: Mon, 28 Sep 2026 00:40:41 +0530 Subject: [PATCH 03/11] Adding Core Plugin Functions --- .../core/AbstractLifecyclePlugin.java | 75 +++++++++++++++++++ .../core/AbstractOnDemandPlugin.java | 62 +++++++++++++++ .../com/iluwatar/microkernel/core/Plugin.java | 66 ++++++++++++++++ .../iluwatar/microkernel/plugins/Plugin.java | 6 -- .../microkernel/registry/PluginRegistry.java | 54 ++++++++++++- 5 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/AbstractLifecyclePlugin.java create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/AbstractOnDemandPlugin.java create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/Plugin.java delete mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/Plugin.java diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/AbstractLifecyclePlugin.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/AbstractLifecyclePlugin.java new file mode 100644 index 000000000000..e0f4a03f3dae --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/AbstractLifecyclePlugin.java @@ -0,0 +1,75 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.core; + +import com.iluwatar.microkernel.ipc.IpcRouter; +import com.iluwatar.microkernel.ipc.Message; + +/** Provides lifecycle hooks for plugins that run automatically while started. */ +public abstract class AbstractLifecyclePlugin implements Plugin { + + protected IpcRouter ipcRouter; + private boolean isStarted = false; + + /** {@inheritDoc} */ + @Override + public void initialize(IpcRouter ipcRouter) { + this.ipcRouter = ipcRouter; + } + + /** Starts the plugin and invokes its startup hook. */ + @Override + public final void onStart() { + this.isStarted = true; + doStart(); // Call the hook method + } + + /** Stops the plugin and invokes its shutdown hook. */ + @Override + public final void onStop() { + this.isStarted = false; + doStop(); // Call the hook method + } + + /** {@inheritDoc} */ + @Override + public boolean isStarted() { + return this.isStarted; + } + + /** Returns a response indicating that this lifecycle plugin ignores messages. */ + @Override + public String handleMessage(Message message) { + // Default behavior for lifecycle plugins: ignore text processing messages + return "IGNORED: Lifecycle plugins typically operate automatically in the background."; + } + + // --- Hook methods for subclasses --- + /** Performs plugin-specific startup work. */ + protected abstract void doStart(); + + /** Performs plugin-specific shutdown work. */ + protected abstract void doStop(); +} diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/AbstractOnDemandPlugin.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/AbstractOnDemandPlugin.java new file mode 100644 index 000000000000..44e71b99b1a8 --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/AbstractOnDemandPlugin.java @@ -0,0 +1,62 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.core; + +import com.iluwatar.microkernel.ipc.IpcRouter; + +/** Provides start-state handling for plugins that act only when messaged. */ +public abstract class AbstractOnDemandPlugin implements Plugin { + + protected IpcRouter ipcRouter; + private boolean isStarted = false; + + /** {@inheritDoc} */ + @Override + public void initialize(IpcRouter ipcRouter) { + this.ipcRouter = ipcRouter; + } + + /** {@inheritDoc} */ + @Override + public void onStart() { + // Just toggle the state; no background task to start + this.isStarted = true; + } + + /** {@inheritDoc} */ + @Override + public void onStop() { + // Just toggle the state; no background task to stop + this.isStarted = false; + } + + /** {@inheritDoc} */ + @Override + public boolean isStarted() { + return this.isStarted; + } + + // handleMessage(Message message) remains abstract and must be implemented by children +} diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/Plugin.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/Plugin.java new file mode 100644 index 000000000000..b02a29042b0b --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/Plugin.java @@ -0,0 +1,66 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.core; + +import com.iluwatar.microkernel.ipc.IpcRouter; +import com.iluwatar.microkernel.ipc.Message; + +/** Defines the lifecycle and message-handling contract for a microkernel plugin. */ +public interface Plugin { + + /** Returns the unique name used to register and address this plugin. */ + String getName(); + + /** Returns a human-readable summary of this plugin. */ + String getDescription(); + + /** + * Provides the router plugins may use to communicate through the kernel. + * + * @param ipcRouter the kernel's message router + */ + void initialize(IpcRouter ipcRouter); + + /** Starts the plugin and allocates any required resources. */ + void onStart(); + + /** Stops the plugin and releases any allocated resources. */ + void onStop(); + + /** + * Returns whether the plugin is started and can receive messages. + * + * @return true if the plugin is started + */ + boolean isStarted(); + + /** + * Handles a message routed to this plugin. + * + * @param message the incoming message + * @return the plugin's response + */ + String handleMessage(Message message); +} diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/Plugin.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/Plugin.java deleted file mode 100644 index c80ccb4f8f62..000000000000 --- a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/Plugin.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.iluwatar.microkernel.plugins; - -public interface Plugin { - - //TODO: Add common functions -} diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/registry/PluginRegistry.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/registry/PluginRegistry.java index 1fe4004ea815..814b62281da2 100644 --- a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/registry/PluginRegistry.java +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/registry/PluginRegistry.java @@ -1,6 +1,58 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ package com.iluwatar.microkernel.registry; +import com.iluwatar.microkernel.core.Plugin; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +/** Stores plugins by name for lookup and message routing. */ public class PluginRegistry { - //TODO: Implement Registry + private final Map plugins = new HashMap<>(); + + /** Registers a plugin under its name, replacing any existing plugin with that name. */ + public void register(Plugin plugin) { + plugins.put(plugin.getName(), plugin); + } + + /** Removes the plugin registered under the given name, if present. */ + public void deregister(String name) { + plugins.remove(name); + } + + /** Returns the plugin registered under the given name, or {@code null} if absent. */ + public Plugin getPlugin(String name) { + return plugins.get(name); + } + + /** Returns an immutable snapshot of all currently registered plugin names. */ + public List getRegisteredPluginNames() { + return plugins.keySet().stream().toList(); + } } From a9b04554cd7bf8ed131396386d51d25bec9f9fb2 Mon Sep 17 00:00:00 2001 From: KishalayP Date: Mon, 28 Sep 2026 00:42:01 +0530 Subject: [PATCH 04/11] Adding Plugins --- .../com/iluwatar/microkernel/ipc/Message.java | 35 +++++++++ .../microkernel/plugins/NeonThemePlugin.java | 61 +++++++++++++++ .../microkernel/plugins/PluginCatalog.java | 78 +++++++++++++++++++ .../plugins/RemoveSpacesPlugin.java | 57 ++++++++++++++ .../microkernel/plugins/UppercasePlugin.java | 55 +++++++++++++ .../microkernel/registry/PluginRegistry.java | 2 +- 6 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/ipc/Message.java create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/NeonThemePlugin.java create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/PluginCatalog.java create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/RemoveSpacesPlugin.java create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/UppercasePlugin.java diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/ipc/Message.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/ipc/Message.java new file mode 100644 index 000000000000..6469b3081c07 --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/ipc/Message.java @@ -0,0 +1,35 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.ipc; + +/** + * Represents a message exchanged between the kernel and a plugin. + * + * @param sender the name of the sender + * @param recipient the name of the intended recipient + * @param action the requested operation + * @param payload the operation data + */ +public record Message(String sender, String recipient, String action, String payload) {} diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/NeonThemePlugin.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/NeonThemePlugin.java new file mode 100644 index 000000000000..82e062eb4e50 --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/NeonThemePlugin.java @@ -0,0 +1,61 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.plugins; + +import com.iluwatar.microkernel.core.AbstractLifecyclePlugin; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class NeonThemePlugin extends AbstractLifecyclePlugin { + + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_GREEN = "\u001B[32m"; + + /** {@inheritDoc} */ + @Override + public String getName() { + return "Theme"; + } + + /** {@inheritDoc} */ + @Override + public String getDescription() { + return "Neon Theme for Editor"; + } + + /** Applies the terminal color used by the neon theme. */ + @Override + protected void doStart() { + System.out.print(ANSI_GREEN); + LOGGER.info("NeonThemePlugin: Neon theme applied."); + } + + /** Restores the terminal's default color. */ + @Override + protected void doStop() { + System.out.print(ANSI_RESET); + LOGGER.info("ThemePlugin: Default theme restored."); + } +} diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/PluginCatalog.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/PluginCatalog.java new file mode 100644 index 000000000000..e33dda92c017 --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/PluginCatalog.java @@ -0,0 +1,78 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.plugins; + +import com.iluwatar.microkernel.core.Plugin; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +/** Provides metadata and factories for the plugins available to the application. */ +public class PluginCatalog { + + private final Map> catalog = new LinkedHashMap<>(); + private final List pluginDisplay = new ArrayList<>(); + + /** Creates the catalog with the module's built-in plugins. */ + public PluginCatalog() { + register(JavaLanguagePlugin::new); + register(NeonThemePlugin::new); + register(UppercasePlugin::new); + register(RemoveSpacesPlugin::new); + } + + /** Helper method to extract the description from the plugin itself. */ + private void register(Supplier factory) { + // Briefly instantiate the plugin to extract its metadata + Plugin metadataInstance = factory.get(); + + // Store it in the map using its own self-reported description as the key + catalog.put(metadataInstance.getName(), factory); + + pluginDisplay.add(metadataInstance.getName() + ": " + metadataInstance.getDescription()); + } + + /** Returns display labels for all available plugins. */ + public List getAvailablePlugins() { + return pluginDisplay; + } + + /** + * Creates a fresh plugin matching a catalog display label. + * + * @param description the plugin name or its displayed name-and-description label + * @return a new plugin instance + * @throws IllegalArgumentException if the label does not identify a catalog plugin + */ + public Plugin createPlugin(String description) { + Supplier factory = catalog.get(description.split(":")[0]); + if (factory != null) { + return factory.get(); + } + throw new IllegalArgumentException("Plugin not found in catalog: " + description); + } +} diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/RemoveSpacesPlugin.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/RemoveSpacesPlugin.java new file mode 100644 index 000000000000..d33eac38e8d0 --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/RemoveSpacesPlugin.java @@ -0,0 +1,57 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.plugins; + +import com.iluwatar.microkernel.core.AbstractOnDemandPlugin; +import com.iluwatar.microkernel.ipc.Message; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RemoveSpacesPlugin extends AbstractOnDemandPlugin { + + private static final Logger log = LoggerFactory.getLogger(RemoveSpacesPlugin.class); + + /** {@inheritDoc} */ + @Override + public String getName() { + return "RemoveSpaces"; + } + + /** {@inheritDoc} */ + @Override + public String getDescription() { + return "Removes Spaces From Texts"; + } + + /** Removes all whitespace from the payload for a transform action. */ + @Override + public String handleMessage(Message message) { + if ("TRANSFORM".equalsIgnoreCase(message.action())) { + log.info("RemoveSpacesPlugin: Stripping whitespace."); + return message.payload().replaceAll("\\s+", ""); + } + return "ERROR: Action not supported."; + } +} diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/UppercasePlugin.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/UppercasePlugin.java new file mode 100644 index 000000000000..89f4154425b2 --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/UppercasePlugin.java @@ -0,0 +1,55 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.plugins; + +import com.iluwatar.microkernel.core.AbstractOnDemandPlugin; +import com.iluwatar.microkernel.ipc.Message; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class UppercasePlugin extends AbstractOnDemandPlugin { + + /** {@inheritDoc} */ + @Override + public String getName() { + return "Uppercase"; + } + + /** {@inheritDoc} */ + @Override + public String getDescription() { + return "Converts Texts to UPPERCASE"; + } + + /** Converts the payload to uppercase for a transform action. */ + @Override + public String handleMessage(Message message) { + if ("TRANSFORM".equalsIgnoreCase(message.action())) { + LOGGER.info("UppercasePlugin: Transforming document."); + return message.payload().toUpperCase(); + } + return "ERROR: Action not supported."; + } +} diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/registry/PluginRegistry.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/registry/PluginRegistry.java index 814b62281da2..aef6caeeadbf 100644 --- a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/registry/PluginRegistry.java +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/registry/PluginRegistry.java @@ -30,8 +30,8 @@ import java.util.Map; import lombok.extern.slf4j.Slf4j; -@Slf4j /** Stores plugins by name for lookup and message routing. */ +@Slf4j public class PluginRegistry { private final Map plugins = new HashMap<>(); From b1dea433217896c8ed3bb7a7d77259b3c1b9ad95 Mon Sep 17 00:00:00 2001 From: KishalayP Date: Mon, 28 Sep 2026 00:42:29 +0530 Subject: [PATCH 05/11] Adding CLI and main app --- .../java/com/iluwatar/microkernel/App.java | 32 ++- .../microkernel/cli/CommandLineInterface.java | 195 ++++++++++++++++++ 2 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/cli/CommandLineInterface.java diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/App.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/App.java index 60c71d1681ff..a6a3cb4dd397 100644 --- a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/App.java +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/App.java @@ -22,12 +22,40 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ + package com.iluwatar.microkernel; +import com.iluwatar.microkernel.cli.CommandLineInterface; +import com.iluwatar.microkernel.core.MicroKernel; +import com.iluwatar.microkernel.ipc.Message; +import com.iluwatar.microkernel.plugins.PluginCatalog; +import lombok.extern.slf4j.Slf4j; + +/** + * Demonstrates a Microkernel architecture featuring a Plugin Registry and Inter-Process + * Communication (IPC). + * + *

The {@link MicroKernel} delegates all feature capabilities to plugins. Plugins do not interact + * by calling methods on one another; instead, they pass {@link Message} records through the + * kernel's IPC router. + */ +@Slf4j public class App { + /** + * Starts the interactive microkernel application. + * + * @param args command-line arguments + */ public static void main(String[] args) { - //TODO: Execute Core Application - } + // 1. Initialize the Core System + var kernel = new MicroKernel(); + var catalog = new PluginCatalog(); + + // 2. Initialize the User Interface, injecting the kernel + var cli = new CommandLineInterface(kernel, catalog); + // 3. Start the application loop + cli.run(); + } } diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/cli/CommandLineInterface.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/cli/CommandLineInterface.java new file mode 100644 index 000000000000..2812ab3852da --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/cli/CommandLineInterface.java @@ -0,0 +1,195 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.cli; + +import com.iluwatar.microkernel.core.MicroKernel; +import com.iluwatar.microkernel.core.Plugin; +import com.iluwatar.microkernel.plugins.PluginCatalog; +import java.util.List; +import java.util.Scanner; + +/** Runs the interactive console interface for the microkernel application. */ +public class CommandLineInterface { + + private final MicroKernel kernel; + private final PluginCatalog catalog; // <-- Injected Catalog + private final Scanner scanner; + + /** + * Creates a command-line interface backed by the given kernel and plugin catalog. + * + * @param kernel the application kernel + * @param catalog the available plugin catalog + */ + public CommandLineInterface(MicroKernel kernel, PluginCatalog catalog) { + this.kernel = kernel; + this.catalog = catalog; + this.scanner = new Scanner(System.in); + } + + /** Displays the main menu and handles commands until the user exits. */ + public void run() { + System.out.println("=== Microkernel IDE Started ==="); + + while (true) { + System.out.println("\n--- Main Menu ---"); + System.out.println("1. List active plugins"); + System.out.println("2. Install a plugin"); + System.out.println("3. Uninstall a plugin"); + System.out.println("4. Open Text Editor"); + System.out.println("5. Exit"); + System.out.print("Select an option (1-5): "); + String input = scanner.nextLine().trim(); + int choice; + try { + choice = Integer.parseInt(input); + } catch (NumberFormatException e) { + System.out.println("Invalid input. Please enter a number."); + continue; + } + + switch (choice) { + case 1 -> listPlugins(); + case 2 -> showInstallMenu(); + case 3 -> showUninstallMenu(); + case 4 -> openTextEditor(); + case 5 -> { + System.out.println("Shutting down kernel... Goodbye!"); + return; + } + default -> System.out.println("Invalid option. Please choose between 1 and 5."); + } + } + } + + private void showInstallMenu() { + System.out.println("\n--- Plugin Marketplace ---"); + + // 1. Ask the catalog what is available + List availablePlugins = catalog.getAvailablePlugins(); + + for (int i = 0; i < availablePlugins.size(); i++) { + System.out.println((i + 1) + ". " + availablePlugins.get(i)); + } + System.out.println((availablePlugins.size() + 1) + ". Cancel"); + System.out.print("Select plugin to install: "); + + try { + int choice = Integer.parseInt(scanner.nextLine().trim()); + + if (choice == availablePlugins.size() + 1) { + return; // Cancelled + } + + if (choice > 0 && choice <= availablePlugins.size()) { + + // 2. Ask the catalog to instantiate the selected plugin + String selected = availablePlugins.get(choice - 1); + Plugin plugin = catalog.createPlugin(selected); + + // 3. Hand the new plugin over to the Kernel + if (kernel.getActivePlugins().contains(plugin.getName())) { + System.out.println("Plugin '" + plugin.getName() + "' is already installed."); + } else { + kernel.loadPlugin(plugin); + kernel.startPlugin(plugin.getName()); + System.out.println("Successfully installed: " + plugin.getName()); + } + } else { + System.out.println("Invalid selection."); + } + } catch (NumberFormatException e) { + System.out.println("Invalid input."); + } + } + + private void openTextEditor() { + System.out.println("\n=== Text Editor Mode ==="); + System.out.println("Type your text line by line. Use the following commands to interact:"); + System.out.println(" :view - View the current document"); + System.out.println(" :clear - Empty the document"); + System.out.println( + " :apply - Dispatch the document to a plugin for transformation"); + System.out.println(" :exit - Return to the Main Menu"); + System.out.println("-------------------------------------------------------------------------"); + + while (true) { + System.out.print("editor> "); + String input = scanner.nextLine(); + + if (input.startsWith(":exit")) { + break; + } else if (input.startsWith(":view")) { + System.out.println("\n--- Current Document Buffer ---"); + System.out.println(kernel.readDocument()); + System.out.println("-------------------------------"); + } else if (input.startsWith(":clear")) { + kernel.clearDocument(); + System.out.println("Document cleared."); + } else if (input.startsWith(":apply ")) { + String[] parts = input.split(" ", 2); + if (parts.length < 2) { + System.out.println("Usage: :apply "); + continue; + } + String result = kernel.transformDocumentWithPlugin(parts[1]); + System.out.println(result); + } else { + kernel.addTextToDocument(input); + } + } + } + + private void listPlugins() { + List activePlugins = kernel.getActivePlugins(); + if (activePlugins.isEmpty()) { + System.out.println("No plugins are currently active."); + } else { + System.out.println("Active Plugins: " + activePlugins); + } + } + + private void showUninstallMenu() { + // unchanged + List activePlugins = kernel.getActivePlugins(); + if (activePlugins.isEmpty()) { + System.out.println("No plugins available to uninstall."); + return; + } + System.out.println("\n--- Uninstall Plugin ---"); + for (int i = 0; i < activePlugins.size(); i++) { + System.out.println((i + 1) + ". " + activePlugins.get(i)); + } + System.out.print("Select plugin to uninstall: "); + try { + int choice = Integer.parseInt(scanner.nextLine().trim()); + if (choice > 0 && choice <= activePlugins.size()) { + kernel.unloadPlugin(activePlugins.get(choice - 1)); + } + } catch (NumberFormatException e) { + System.out.println("Invalid input."); + } + } +} From 24387f96771a67e8714031a789d10f0644617089 Mon Sep 17 00:00:00 2001 From: KishalayP Date: Mon, 28 Sep 2026 00:42:55 +0530 Subject: [PATCH 06/11] Adding Unit Tests --- .../etc/microkernel-architecture.png | Bin 0 -> 55007 bytes .../microkernel/core/CoreService.java | 6 - .../com/iluwatar/microkernel/AppTest.java | 30 ++++- .../cli/CommandLineInterfaceTest.java | 85 +++++++++++++ .../microkernel/core/MicroKernelTest.java | 116 ++++++++++++++++++ .../microkernel/ipc/IpcRouterTest.java | 104 ++++++++++++++++ .../plugins/PluginCatalogTest.java | 64 ++++++++++ .../plugins/PluginImplementationsTest.java | 93 ++++++++++++++ .../registry/PluginRegistryTest.java | 63 ++++++++++ 9 files changed, 552 insertions(+), 9 deletions(-) create mode 100644 microkernel-architecture/etc/microkernel-architecture.png delete mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/core/CoreService.java create mode 100644 microkernel-architecture/src/test/java/com/iluwatar/microkernel/cli/CommandLineInterfaceTest.java create mode 100644 microkernel-architecture/src/test/java/com/iluwatar/microkernel/core/MicroKernelTest.java create mode 100644 microkernel-architecture/src/test/java/com/iluwatar/microkernel/ipc/IpcRouterTest.java create mode 100644 microkernel-architecture/src/test/java/com/iluwatar/microkernel/plugins/PluginCatalogTest.java create mode 100644 microkernel-architecture/src/test/java/com/iluwatar/microkernel/plugins/PluginImplementationsTest.java create mode 100644 microkernel-architecture/src/test/java/com/iluwatar/microkernel/registry/PluginRegistryTest.java diff --git a/microkernel-architecture/etc/microkernel-architecture.png b/microkernel-architecture/etc/microkernel-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..43c731f1c6555592207a1ff69645dc977cc7a3cb GIT binary patch literal 55007 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 Yu)X!Y5~XWaCIRsJ)7<(@g_&#AKl~ catalog.createPlugin("Unknown")); + + assertEquals("Plugin not found in catalog: Unknown", exception.getMessage()); + } +} diff --git a/microkernel-architecture/src/test/java/com/iluwatar/microkernel/plugins/PluginImplementationsTest.java b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/plugins/PluginImplementationsTest.java new file mode 100644 index 000000000000..e382c45ac3a0 --- /dev/null +++ b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/plugins/PluginImplementationsTest.java @@ -0,0 +1,93 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.plugins; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.iluwatar.microkernel.ipc.Message; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class PluginImplementationsTest { + + @Test + void uppercasePluginTransformsOnlyTransformActions() { + var plugin = new UppercasePlugin(); + + assertEquals("HELLO", plugin.handleMessage(message("TRANSFORM", "hello"))); + assertEquals("ERROR: Action not supported.", plugin.handleMessage(message("OTHER", "hello"))); + assertEquals("Uppercase", plugin.getName()); + assertFalse(plugin.isStarted()); + } + + @Test + void removeSpacesPluginRemovesAllWhitespace() { + var plugin = new RemoveSpacesPlugin(); + + assertEquals("onetwo", plugin.handleMessage(message("TRANSFORM", "one \t two"))); + assertEquals("ERROR: Action not supported.", plugin.handleMessage(message("OTHER", "a b"))); + } + + @Test + void javaLanguagePluginFormatsTransformPayload() { + var plugin = new JavaLanguagePlugin(); + + assertEquals( + "class A {\n int x;\n \n}", + plugin.handleMessage(message("TRANSFORM", "class A {int x;}"))); + assertEquals("ERROR: Action not supported.", plugin.handleMessage(message("OTHER", "text"))); + } + + @Test + void lifecyclePluginRunsHooksAndIgnoresMessages() { + var plugin = new NeonThemePlugin(); + var output = new ByteArrayOutputStream(); + PrintStream originalOutput = System.out; + + try { + System.setOut(new PrintStream(output, true, StandardCharsets.UTF_8)); + plugin.onStart(); + assertTrue(plugin.isStarted()); + assertEquals( + "IGNORED: Lifecycle plugins typically operate automatically in the background.", + plugin.handleMessage(message("TRANSFORM", "text"))); + plugin.onStop(); + assertFalse(plugin.isStarted()); + } finally { + System.setOut(originalOutput); + } + + assertTrue(output.toString(StandardCharsets.UTF_8).contains("\u001B[32m")); + assertTrue(output.toString(StandardCharsets.UTF_8).contains("\u001B[0m")); + } + + private static Message message(String action, String payload) { + return new Message("test", "plugin", action, payload); + } +} diff --git a/microkernel-architecture/src/test/java/com/iluwatar/microkernel/registry/PluginRegistryTest.java b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/registry/PluginRegistryTest.java new file mode 100644 index 000000000000..51d4ff257b0e --- /dev/null +++ b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/registry/PluginRegistryTest.java @@ -0,0 +1,63 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.registry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.iluwatar.microkernel.core.Plugin; +import com.iluwatar.microkernel.plugins.UppercasePlugin; +import java.util.List; +import org.junit.jupiter.api.Test; + +class PluginRegistryTest { + + @Test + void registersReplacesLooksUpAndDeregistersPlugins() { + var registry = new PluginRegistry(); + Plugin first = new UppercasePlugin(); + Plugin replacement = + new UppercasePlugin() { + @Override + public String getDescription() { + return "replacement"; + } + }; + + registry.register(first); + assertSame(first, registry.getPlugin("Uppercase")); + assertEquals(List.of("Uppercase"), registry.getRegisteredPluginNames()); + + registry.register(replacement); + assertSame(replacement, registry.getPlugin("Uppercase")); + + registry.deregister("Uppercase"); + registry.deregister("Uppercase"); + assertNull(registry.getPlugin("Uppercase")); + assertTrue(registry.getRegisteredPluginNames().isEmpty()); + } +} From 29f5c0d691a258f248d1ec622024e53079903790 Mon Sep 17 00:00:00 2001 From: KishalayP Date: Mon, 28 Sep 2026 00:43:46 +0530 Subject: [PATCH 07/11] Reformatted as per style guide --- microkernel-architecture/pom.xml | 56 +++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/microkernel-architecture/pom.xml b/microkernel-architecture/pom.xml index c379b6a41991..9e50dcdb820a 100644 --- a/microkernel-architecture/pom.xml +++ b/microkernel-architecture/pom.xml @@ -26,17 +26,10 @@ --> + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> microkernel-architecture - 4.0.0 - - java-design-patterns - com.iluwatar - 1.26.0-SNAPSHOT - - @@ -54,9 +47,36 @@ org.apache.maven.plugins + + com.diffplug.spotless + spotless-maven-plugin + 3.8.0 + + + + check + apply + + + + + + + 1.28.0 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 21 + 21 + + - junit-jupiter-engine @@ -64,9 +84,21 @@ test - h2 - com.h2database + slf4j-api + org.slf4j + + + logback-classic + ch.qos.logback + 4.0.0 + + + java-design-patterns + com.iluwatar + 1.26.0-SNAPSHOT + + From 6d7d86a9f5f4fa14f58279dba31f4949725b3e53 Mon Sep 17 00:00:00 2001 From: KishalayP Date: Mon, 28 Sep 2026 00:43:50 +0530 Subject: [PATCH 08/11] Reformatted as per style guide --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 17cca3d15482..56717625f7e3 100644 --- a/pom.xml +++ b/pom.xml @@ -164,6 +164,7 @@ mediator memento metadata-mapping + microkernel-architecture microservices-aggregrator microservices-api-gateway microservices-client-side-ui-composition From 6b1a0d8cc9d179697efb451ab79164590c69115c Mon Sep 17 00:00:00 2001 From: KishalayP Date: Mon, 28 Sep 2026 00:44:00 +0530 Subject: [PATCH 09/11] Adding Plugin --- .../plugins/JavaLanguagePlugin.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/JavaLanguagePlugin.java diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/JavaLanguagePlugin.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/JavaLanguagePlugin.java new file mode 100644 index 000000000000..976b2f65a161 --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/plugins/JavaLanguagePlugin.java @@ -0,0 +1,55 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.microkernel.plugins; + +import com.iluwatar.microkernel.core.AbstractOnDemandPlugin; +import com.iluwatar.microkernel.ipc.Message; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class JavaLanguagePlugin extends AbstractOnDemandPlugin { + + /** {@inheritDoc} */ + @Override + public String getName() { + return "Java"; + } + + /** {@inheritDoc} */ + @Override + public String getDescription() { + return "Java Formatting Support (Java)"; + } + + /** Applies the example Java formatter for a transform action. */ + @Override + public String handleMessage(Message message) { + if ("TRANSFORM".equalsIgnoreCase(message.action())) { + LOGGER.info("JavaPlugin: Formatting code."); + return message.payload().replace("{", "{\n ").replace(";", ";\n ").replace("}", "\n}"); + } + return "ERROR: Action not supported."; + } +} From ddba2f62f3df5ffea3b489f19b0f482eefcaf827 Mon Sep 17 00:00:00 2001 From: KishalayP Date: Mon, 28 Sep 2026 00:44:26 +0530 Subject: [PATCH 10/11] Adding Details for microkernel-architecture --- microkernel-architecture/README.md | 96 +++++++++++++++++ .../etc/microkernel-architecture.puml | 101 ++++++++++++++++++ .../etc/microkernel-flow.png | Bin 0 -> 18870 bytes .../etc/microkernel-flow.puml | 57 ++++++++++ 4 files changed, 254 insertions(+) create mode 100644 microkernel-architecture/etc/microkernel-architecture.puml create mode 100644 microkernel-architecture/etc/microkernel-flow.png create mode 100644 microkernel-architecture/etc/microkernel-flow.puml diff --git a/microkernel-architecture/README.md b/microkernel-architecture/README.md index 3c3382a701f5..2421f0f208ef 100644 --- a/microkernel-architecture/README.md +++ b/microkernel-architecture/README.md @@ -12,16 +12,112 @@ tag: ## Also known as +* Plug-in architecture + ## Intent of Microkernel architecture Design Pattern +Separate an application's minimal core from optional features so functionality can be added, replaced, or removed through plugins without changing the core. This provides extensibility while keeping specialized processing isolated from general application logic. + ## Detailed Explanation of Microkernel architecture Pattern with Real-World Examples +Real-world example + +> An IDE can provide a basic editor as its core while installing language tools, debuggers, and integrations as plugins. Likewise, a document-processing application can route documents to format-specific converters. In an insurance claims system, plugins can encapsulate state-specific rules without complicating the core claims workflow. + +In plain words + +> The microkernel provides the essential services and lets independent plugins supply optional capabilities through a small shared interface. The core manages plugin registration, lifecycle, and communication, while each plugin owns its specialized behavior. + +Architecture + +![Microkernel architecture process flow](etc/microkernel-flow.png) + +The plugin class structure is documented separately in [microkernel-architecture.png](etc/microkernel-architecture.png). +## Programmatic Example of Microkernel architecture Pattern in Java + +This example is an interactive text editor. The `MicroKernel` owns the document buffer and plugin lifecycle. The `PluginCatalog` supplies plugin factories, and the `PluginRegistry` keeps track of installed plugins. To transform the document, the kernel packages the document text in a `Message` and sends it through the `IpcRouter`; the selected plugin returns the transformed text. + +Every plugin follows a common contract: + +```java +public interface Plugin { + String getName(); + String getDescription(); + void initialize(IpcRouter ipcRouter); + void onStart(); + void onStop(); + boolean isStarted(); + String handleMessage(Message message); +} +``` + +The kernel routes a transformation request without depending on the implementation of the plugin: + +```java +public String transformDocumentWithPlugin(String pluginName) { + Message message = + new Message("Kernel", pluginName, "TRANSFORM", documentBuffer.toString()); + String transformedText = ipcRouter.sendMessage(message); + + clearDocument(); + documentBuffer.append(transformedText); + return "Success: Document transformed by " + pluginName; +} +``` + +Plugins such as `UppercasePlugin` and `RemoveSpacesPlugin` implement the contract and handle the `TRANSFORM` action independently. The catalog creates fresh plugin instances from registered factories, while the kernel initializes, starts, and unloads installed plugins. + +Start the application and select **Open Text Editor**. Type a few lines, then use `:view`, `:clear`, or `:apply ` to interact with the document. The available plugin list is shown in the install menu. + +The example registers plugin factories directly in `PluginCatalog`; a production system could instead discover plugins from configuration, Java's service loader, or a plugin container. + ## When to Use the Microkernel architecture Pattern in Java +* Use when building a product with optional capabilities that vary by customer, deployment, or installation. +* Use when extensions should be developed, tested, and released independently behind a well-defined interface. +* Use when volatile custom rules or specialized processing should be isolated from general business logic. +* Avoid it when there are few optional features or when plugins need deep access to core internals; the plugin API and lifecycle add ongoing maintenance costs. + +## Architecture Characteristics + +* **Agility: High.** Changes can often be isolated to loosely coupled plugin modules. +* **Ease of deployment: High.** Plugins can be installed or updated separately; runtime dynamic loading depends on the implementation. +* **Testability: High.** Plugins can be tested independently, and the core can be tested against plugin contracts. +* **Performance: High.** The application can include only the features needed for a product variant, although routing and plugin boundaries still have costs. +* **Scalability: Low to moderate.** A microkernel is commonly deployed as a single application, so large-scale distribution is not its main strength. +* **Ease of development: Low to moderate.** Plugin contracts, compatibility, lifecycle, and failure handling require deliberate design. + ## Real-World Applications of Microkernel architecture Pattern in Java +* IDEs such as Eclipse, which extend core editing capabilities with plugins. +* Internet browsers that add capabilities through extensions. +* Insurance claims processing systems that isolate state-specific rules from general processing. +* Operating systems and product platforms that provide optional or customer-specific modules. +* Data-processing applications that load format-specific readers, writers, or transformations. + ## Benefits and Trade-offs of Microkernel architecture Pattern +Benefits: + +* Keeps the core small and focused on essential responsibilities. +* Makes optional features replaceable and independently testable. +* Supports product variants without duplicating the core application. + +Trade-offs: + +* Plugin contracts need compatibility and versioning as the system evolves. +* Plugin discovery, loading, failure handling, and security require deliberate design in production systems. +* Indirection through a registry and message router can make execution flow less obvious than direct calls. +* A single-core deployment may limit horizontal scalability. + ## Related Java Design Patterns +* [Strategy](https://java-design-patterns.com/patterns/strategy/): Plugins can provide interchangeable strategies for a core operation. +* [Factory Kit](https://java-design-patterns.com/patterns/factory-kit/): Can help create plugin implementations from configuration. +* [Dependency Injection](https://java-design-patterns.com/patterns/dependency-injection/): Can be used to assemble the core and its plugins. + ## References and Credits + +* Richards, Mark. *Software Architecture Patterns*. O'Reilly Media, Inc., 2015. +* [Pattern-Oriented Software Architecture, Volume 1](https://www.wiley.com/en-us/Pattern+Oriented+Software+Architecture%2C+Volume+1%3A+A+System+of+Patterns-p-9780471958697) +* [Microkernel architecture style (Microsoft Azure Architecture Center)](https://learn.microsoft.com/en-us/azure/architecture/guide/architecture-styles/microkernel) diff --git a/microkernel-architecture/etc/microkernel-architecture.puml b/microkernel-architecture/etc/microkernel-architecture.puml new file mode 100644 index 000000000000..8c4417c1da44 --- /dev/null +++ b/microkernel-architecture/etc/microkernel-architecture.puml @@ -0,0 +1,101 @@ +@startuml +title Microkernel Architecture - Class Structure + +top to bottom direction +skinparam classAttributeIconSize 0 +skinparam linetype ortho +skinparam nodesep 35 +skinparam ranksep 45 + +package "Application" { + class App { + +main(args: String[]): void + } + + class CommandLineInterface { + -kernel: MicroKernel + -catalog: PluginCatalog + +run(): void + } + + class PluginCatalog { + -catalog: Map~String, Supplier~Plugin~~ + +getAvailablePlugins(): List~String~ + +createPlugin(description: String): Plugin + } +} + +package "Kernel" { + class MicroKernel { + -registry: PluginRegistry + -ipcRouter: IpcRouter + -documentBuffer: StringBuilder + +loadPlugin(plugin: Plugin): void + +startPlugin(name: String): void + +unloadPlugin(name: String): void + +transformDocumentWithPlugin(name: String): String + } + + class PluginRegistry { + -plugins: Map~String, Plugin~ + +register(plugin: Plugin): void + +deregister(name: String): void + +getPlugin(name: String): Plugin + } + + class IpcRouter { + -registry: PluginRegistry + +sendMessage(message: Message): String + } +} + +package "Plugin API" { + interface Plugin { + +getName(): String + +initialize(router: IpcRouter): void + +onStart(): void + +onStop(): void + +handleMessage(message: Message): String + } + + abstract class AbstractOnDemandPlugin + abstract class AbstractLifecyclePlugin +} + +package "Built-in Plugins" { + class UppercasePlugin + class RemoveSpacesPlugin + class JavaLanguagePlugin + class NeonThemePlugin +} + +class Message <> { + sender: String + recipient: String + action: String + payload: String +} + +App --> CommandLineInterface +CommandLineInterface --> MicroKernel +CommandLineInterface --> PluginCatalog +PluginCatalog ..> Plugin : creates + +MicroKernel --> PluginRegistry : registers / removes by name +MicroKernel --> IpcRouter : sends messages +IpcRouter --> PluginRegistry : looks up recipient +PluginRegistry "1" --> "0..*" Plugin : maps names to instances +IpcRouter ..> Plugin : dispatches resolved instance + +AbstractOnDemandPlugin ..|> Plugin +AbstractLifecyclePlugin ..|> Plugin +UppercasePlugin --|> AbstractOnDemandPlugin +RemoveSpacesPlugin --|> AbstractOnDemandPlugin +JavaLanguagePlugin --|> AbstractOnDemandPlugin +NeonThemePlugin --|> AbstractLifecyclePlugin + +MicroKernel ..> Message : creates +IpcRouter ..> Message : routes +Plugin ..> Message : handles + +@enduml diff --git a/microkernel-architecture/etc/microkernel-flow.png b/microkernel-architecture/etc/microkernel-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..c3b398d04855495046294d3d6fe0da5a8c667796 GIT binary patch literal 18870 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 vW?L^(_GR;TQn~lNPx*yhJJ{vN{ CLI : new plugin\ninstance +CLI -left-> Catalog : list or\ncreate + +' NEW: Show the Catalog as the factory for Plugins +Catalog -down-> Plugin : instantiates\n(factory) + +CLI -down-> Kernel : request document transform\ninstall, start, or remove +Kernel -up-> CLI : updated document\n& status + +' --- LEVEL 2 & 3: Core to Internal Services --- +Kernel -down-> Registry : register or deregister\nby name +Kernel -down-> Router : message with\nrecipient name +Router -up-> Kernel : result or\ndelivery error + +' --- LEVEL 3: Internal Service Communication --- +Router -left-> Registry : look up\nrecipient +Registry -right-> Router : matching plugin\nreference + +' --- LEVEL 3 & 4: Services to Plugin --- +Registry -down-> Plugin : stores and\nresolves by name +Router -down-> Plugin : dispatch if\nstarted +Plugin -up-> Router : transformation\nresult + +@enduml \ No newline at end of file From 46ce10aaf4168bf92b922de02f5a0fcf61360ddb Mon Sep 17 00:00:00 2001 From: KishalayP Date: Mon, 28 Sep 2026 18:37:16 +0530 Subject: [PATCH 11/11] Adding Unit Tests --- .../microkernel/cli/CommandLineInterface.java | 4 +- .../com/iluwatar/microkernel/AppTest.java | 6 +++ .../cli/CommandLineInterfaceTest.java | 47 +++++++++++++++++++ .../plugins/PluginImplementationsTest.java | 37 +++++++++++++++ 4 files changed, 92 insertions(+), 2 deletions(-) diff --git a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/cli/CommandLineInterface.java b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/cli/CommandLineInterface.java index 2812ab3852da..de5fed7e716d 100644 --- a/microkernel-architecture/src/main/java/com/iluwatar/microkernel/cli/CommandLineInterface.java +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/cli/CommandLineInterface.java @@ -34,7 +34,7 @@ public class CommandLineInterface { private final MicroKernel kernel; - private final PluginCatalog catalog; // <-- Injected Catalog + private final PluginCatalog catalog; private final Scanner scanner; /** @@ -150,7 +150,7 @@ private void openTextEditor() { System.out.println("Document cleared."); } else if (input.startsWith(":apply ")) { String[] parts = input.split(" ", 2); - if (parts.length < 2) { + if (parts[1].isBlank()) { System.out.println("Usage: :apply "); continue; } diff --git a/microkernel-architecture/src/test/java/com/iluwatar/microkernel/AppTest.java b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/AppTest.java index 2aea03010e9a..d7c7004f1544 100644 --- a/microkernel-architecture/src/test/java/com/iluwatar/microkernel/AppTest.java +++ b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/AppTest.java @@ -25,6 +25,7 @@ package com.iluwatar.microkernel; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; @@ -36,6 +37,11 @@ class AppTest { + @Test + void canBeInstantiated() { + assertNotNull(new App()); + } + @Test void mainStartsAndExitsTheApplication() { InputStream originalInput = System.in; diff --git a/microkernel-architecture/src/test/java/com/iluwatar/microkernel/cli/CommandLineInterfaceTest.java b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/cli/CommandLineInterfaceTest.java index 6ba649986e8c..8da22bb29a9d 100644 --- a/microkernel-architecture/src/test/java/com/iluwatar/microkernel/cli/CommandLineInterfaceTest.java +++ b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/cli/CommandLineInterfaceTest.java @@ -48,6 +48,37 @@ void installsListsAndUninstallsPlugin() { assertTrue(kernel.getActivePlugins().isEmpty()); } + @Test + void installsDuplicateAndUninstallsPlugin() { + var kernel = new MicroKernel(); + var output = runCli(kernel, "2\n3\n2\n3\n1\n3\n1\n1\n5\n"); + + assertTrue(output.contains("Successfully installed: Uppercase")); + assertTrue(output.contains("Plugin 'Uppercase' is already installed.")); + assertTrue(output.contains("Active Plugins: [Uppercase]")); + assertTrue(output.contains("No plugins are currently active.")); + assertTrue(kernel.getActivePlugins().isEmpty()); + } + + @Test + void reportsNoPluginsToUninstall() { + var output = runCli(new MicroKernel(), "3\n5\n"); + + assertTrue(output.contains("No plugins available to uninstall.")); + } + + @Test + void reportsInvalidInstallAndUninstallSelections() { + var kernel = new MicroKernel(); + var output = + runCli(kernel, "2\ninvalid\n2\n0\n2\n5\n2\n6\n2\n3\n3\ninvalid\n3\n0\n3\n2\n3\n1\n5\n"); + + assertTrue(output.contains("Invalid input.")); + assertTrue(output.contains("Invalid selection.")); + assertTrue(output.contains("Successfully installed: Uppercase")); + assertTrue(kernel.getActivePlugins().isEmpty()); + } + @Test void editsAndTransformsDocument() { var kernel = new MicroKernel(); @@ -58,6 +89,22 @@ void editsAndTransformsDocument() { assertEquals("HELLO WORLD\n", kernel.readDocument()); } + @Test + void clearsDocumentAndReportsUnknownPlugin() { + var kernel = new MicroKernel(); + var output = + runCli( + kernel, + "2\n3\n4\nold text\n:clear\n:apply \nnew text\n:apply Missing\n:apply Uppercase\n:view\n:exit\n5\n"); + + assertTrue(output.contains("Document cleared.")); + assertTrue(output.contains("Usage: :apply ")); + assertTrue(output.contains("ERROR: Plugin 'Missing' is not active")); + assertTrue(output.contains("Success: Document transformed by Uppercase")); + assertTrue(output.contains("NEW TEXT")); + assertEquals("NEW TEXT\n", kernel.readDocument()); + } + @Test void reportsInvalidMenuInputAndChoice() { var output = runCli(new MicroKernel(), "invalid\n9\n5\n"); diff --git a/microkernel-architecture/src/test/java/com/iluwatar/microkernel/plugins/PluginImplementationsTest.java b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/plugins/PluginImplementationsTest.java index e382c45ac3a0..c7f690c042f8 100644 --- a/microkernel-architecture/src/test/java/com/iluwatar/microkernel/plugins/PluginImplementationsTest.java +++ b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/plugins/PluginImplementationsTest.java @@ -26,9 +26,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.iluwatar.microkernel.core.AbstractLifecyclePlugin; +import com.iluwatar.microkernel.ipc.IpcRouter; import com.iluwatar.microkernel.ipc.Message; +import com.iluwatar.microkernel.registry.PluginRegistry; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; @@ -87,7 +91,40 @@ void lifecyclePluginRunsHooksAndIgnoresMessages() { assertTrue(output.toString(StandardCharsets.UTF_8).contains("\u001B[0m")); } + @Test + void lifecyclePluginStoresIpcRouterDuringInitialization() { + var plugin = new TestLifecyclePlugin(); + var router = new IpcRouter(new PluginRegistry()); + + plugin.initialize(router); + + assertSame(router, plugin.getIpcRouter()); + } + private static Message message(String action, String payload) { return new Message("test", "plugin", action, payload); } + + private static class TestLifecyclePlugin extends AbstractLifecyclePlugin { + + @Override + public String getName() { + return "Test"; + } + + @Override + public String getDescription() { + return "Test lifecycle plugin"; + } + + @Override + protected void doStart() {} + + @Override + protected void doStop() {} + + private IpcRouter getIpcRouter() { + return ipcRouter; + } + } }