diff --git a/microkernel-architecture/README.md b/microkernel-architecture/README.md new file mode 100644 index 000000000000..2421f0f208ef --- /dev/null +++ b/microkernel-architecture/README.md @@ -0,0 +1,123 @@ +--- +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 + +* 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.png b/microkernel-architecture/etc/microkernel-architecture.png new file mode 100644 index 000000000000..43c731f1c655 Binary files /dev/null and b/microkernel-architecture/etc/microkernel-architecture.png differ 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 000000000000..c3b398d04855 Binary files /dev/null and b/microkernel-architecture/etc/microkernel-flow.png differ diff --git a/microkernel-architecture/etc/microkernel-flow.puml b/microkernel-architecture/etc/microkernel-flow.puml new file mode 100644 index 000000000000..29f132845367 --- /dev/null +++ b/microkernel-architecture/etc/microkernel-flow.puml @@ -0,0 +1,57 @@ +@startuml +' Force straight 90-degree lines +skinparam linetype ortho + +' Drastically increase separation to prevent text overlapping +skinparam nodesep 160 +skinparam ranksep 160 +skinparam padding 12 + +' Mimic the standard Java Design Patterns (URM) color scheme +skinparam rectangle { + BackgroundColor #FEFECE + BorderColor #A80036 + BorderThickness 1.5 + FontColor #000000 + RoundCorner 8 +} + +skinparam arrow { + Color #A80036 + Thickness 1.5 + FontColor #444444 + FontSize 12 +} + +rectangle "Plugin catalog" as Catalog +rectangle "Command-line interface" as CLI +rectangle "Microkernel" as Kernel +rectangle "Plugin registry" as Registry +rectangle "IPC router" as Router +rectangle "Installed plugin instance" as Plugin + +' --- LEVEL 1 & 2: User Interface to Core --- +Catalog -right-> 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 diff --git a/microkernel-architecture/pom.xml b/microkernel-architecture/pom.xml new file mode 100644 index 000000000000..9e50dcdb820a --- /dev/null +++ b/microkernel-architecture/pom.xml @@ -0,0 +1,104 @@ + + + + + microkernel-architecture + + + + maven-assembly-plugin + + + + + + com.iluwatar.microkernel.App + + + + + + 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 + org.junit.jupiter + test + + + slf4j-api + org.slf4j + + + logback-classic + ch.qos.logback + + + + 4.0.0 + + + java-design-patterns + com.iluwatar + 1.26.0-SNAPSHOT + + + 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..a6a3cb4dd397 --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/App.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; + +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) { + // 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..de5fed7e716d --- /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; + 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[1].isBlank()) { + 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."); + } + } +} 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/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/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/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"; + } + } +} 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/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."; + } +} 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 new file mode 100644 index 000000000000..aef6caeeadbf --- /dev/null +++ b/microkernel-architecture/src/main/java/com/iluwatar/microkernel/registry/PluginRegistry.java @@ -0,0 +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; + +/** Stores plugins by name for lookup and message routing. */ +@Slf4j +public class PluginRegistry { + + 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(); + } +} 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..d7c7004f1544 --- /dev/null +++ b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/AppTest.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; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class AppTest { + + @Test + void canBeInstantiated() { + assertNotNull(new App()); + } + + @Test + void mainStartsAndExitsTheApplication() { + InputStream originalInput = System.in; + PrintStream originalOutput = System.out; + var output = new ByteArrayOutputStream(); + + try { + System.setIn(new ByteArrayInputStream("5\n".getBytes(StandardCharsets.UTF_8))); + System.setOut(new PrintStream(output, true, StandardCharsets.UTF_8)); + + App.main(new String[0]); + } finally { + System.setIn(originalInput); + System.setOut(originalOutput); + } + + assertTrue(output.toString(StandardCharsets.UTF_8).contains("Shutting down kernel")); + } +} 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 new file mode 100644 index 000000000000..8da22bb29a9d --- /dev/null +++ b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/cli/CommandLineInterfaceTest.java @@ -0,0 +1,132 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.iluwatar.microkernel.core.MicroKernel; +import com.iluwatar.microkernel.plugins.PluginCatalog; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class CommandLineInterfaceTest { + + @Test + void installsListsAndUninstallsPlugin() { + var kernel = new MicroKernel(); + var output = runCli(kernel, "2\n3\n3\n1\n1\n5\n"); + + assertTrue(output.contains("Successfully installed: Uppercase")); + assertTrue(output.contains("No plugins are currently active.")); + 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(); + var output = runCli(kernel, "2\n3\n4\nhello world\n:apply Uppercase\n:view\n:exit\n5\n"); + + assertTrue(output.contains("Success: Document transformed by Uppercase")); + assertTrue(output.contains("HELLO WORLD")); + 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"); + + assertTrue(output.contains("Invalid input. Please enter a number.")); + assertTrue(output.contains("Invalid option. Please choose between 1 and 5.")); + } + + private static String runCli(MicroKernel kernel, String input) { + InputStream originalInput = System.in; + PrintStream originalOutput = System.out; + var output = new ByteArrayOutputStream(); + + try { + System.setIn(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8))); + System.setOut(new PrintStream(output, true, StandardCharsets.UTF_8)); + new CommandLineInterface(kernel, new PluginCatalog()).run(); + } finally { + System.setIn(originalInput); + System.setOut(originalOutput); + } + + return output.toString(StandardCharsets.UTF_8); + } +} diff --git a/microkernel-architecture/src/test/java/com/iluwatar/microkernel/core/MicroKernelTest.java b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/core/MicroKernelTest.java new file mode 100644 index 000000000000..b851bf88f0e6 --- /dev/null +++ b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/core/MicroKernelTest.java @@ -0,0 +1,116 @@ +/* + * 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 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 com.iluwatar.microkernel.plugins.UppercasePlugin; +import org.junit.jupiter.api.Test; + +class MicroKernelTest { + + @Test + void loadsStartsAndUnloadsPlugin() { + var kernel = new MicroKernel(); + var plugin = new UppercasePlugin(); + + kernel.loadPlugin(plugin); + assertEquals(java.util.List.of("Uppercase"), kernel.getActivePlugins()); + assertFalse(plugin.isStarted()); + + kernel.startPlugin(plugin.getName()); + assertTrue(plugin.isStarted()); + + kernel.unloadPlugin(plugin.getName()); + assertFalse(plugin.isStarted()); + assertTrue(kernel.getActivePlugins().isEmpty()); + } + + @Test + void transformsAndReplacesDocument() { + var kernel = new MicroKernel(); + var plugin = new UppercasePlugin(); + kernel.loadPlugin(plugin); + kernel.startPlugin(plugin.getName()); + kernel.addTextToDocument("hello microkernel"); + + assertEquals( + "Success: Document transformed by Uppercase", + kernel.transformDocumentWithPlugin("Uppercase")); + assertEquals("HELLO MICROKERNEL\n", kernel.readDocument()); + } + + @Test + void rejectsUnknownOrStoppedPluginWithoutChangingDocument() { + var kernel = new MicroKernel(); + var plugin = new UppercasePlugin(); + kernel.addTextToDocument("unchanged"); + + assertEquals( + "ERROR: Plugin 'Uppercase' is not active", kernel.transformDocumentWithPlugin("Uppercase")); + + kernel.loadPlugin(plugin); + assertEquals( + "Transformation failed: ERROR: Plugin Stopped", + kernel.transformDocumentWithPlugin("Uppercase")); + assertEquals("unchanged\n", kernel.readDocument()); + } + + @Test + void clearsDocumentAndIgnoresUnknownLifecycleRequests() { + var kernel = new MicroKernel(); + kernel.addTextToDocument("text"); + + kernel.startPlugin("missing"); + kernel.unloadPlugin("missing"); + kernel.clearDocument(); + + assertEquals("", kernel.readDocument()); + assertTrue(kernel.getActivePlugins().isEmpty()); + } + + @Test + void routesAPluginResponseThatRepresentsFailureWithoutOverwritingDocument() { + var kernel = new MicroKernel(); + Plugin plugin = + new UppercasePlugin() { + @Override + public String handleMessage(Message message) { + return "ERROR: failed"; + } + }; + kernel.loadPlugin(plugin); + kernel.startPlugin(plugin.getName()); + kernel.addTextToDocument("preserve me"); + + assertEquals( + "Transformation failed: ERROR: failed", + kernel.transformDocumentWithPlugin(plugin.getName())); + assertEquals("preserve me\n", kernel.readDocument()); + } +} diff --git a/microkernel-architecture/src/test/java/com/iluwatar/microkernel/ipc/IpcRouterTest.java b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/ipc/IpcRouterTest.java new file mode 100644 index 000000000000..f6a3a03f5ddf --- /dev/null +++ b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/ipc/IpcRouterTest.java @@ -0,0 +1,104 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; + +import com.iluwatar.microkernel.core.AbstractOnDemandPlugin; +import com.iluwatar.microkernel.registry.PluginRegistry; +import org.junit.jupiter.api.Test; + +class IpcRouterTest { + + private final PluginRegistry registry = new PluginRegistry(); + private final IpcRouter router = new IpcRouter(registry); + + @Test + void reportsMissingRecipient() { + assertEquals( + "ERROR: Destination Unreachable", + router.sendMessage(new Message("sender", "missing", "ACT", "payload"))); + } + + @Test + void reportsStoppedRecipient() { + registry.register(new TestPlugin(false, false)); + + assertEquals( + "ERROR: Plugin Stopped", + router.sendMessage(new Message("sender", "test", "ACT", "payload"))); + } + + @Test + void deliversMessageToStartedRecipient() { + registry.register(new TestPlugin(true, false)); + + assertEquals( + "response:payload", router.sendMessage(new Message("sender", "test", "ACT", "payload"))); + } + + @Test + void isolatesPluginFailure() { + registry.register(new TestPlugin(true, true)); + + assertEquals( + "ERROR: Plugin Fault Isolated", + router.sendMessage(new Message("sender", "test", "ACT", "payload"))); + } + + private static class TestPlugin extends AbstractOnDemandPlugin { + + private final boolean started; + private final boolean fail; + + private TestPlugin(boolean started, boolean fail) { + this.started = started; + this.fail = fail; + } + + @Override + public String getName() { + return "test"; + } + + @Override + public String getDescription() { + return "test plugin"; + } + + @Override + public boolean isStarted() { + return started; + } + + @Override + public String handleMessage(Message message) { + if (fail) { + throw new IllegalStateException("test failure"); + } + return "response:" + message.payload(); + } + } +} diff --git a/microkernel-architecture/src/test/java/com/iluwatar/microkernel/plugins/PluginCatalogTest.java b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/plugins/PluginCatalogTest.java new file mode 100644 index 000000000000..673538b1b309 --- /dev/null +++ b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/plugins/PluginCatalogTest.java @@ -0,0 +1,64 @@ +/* + * 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.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class PluginCatalogTest { + + @Test + void listsBuiltInPlugins() { + var catalog = new PluginCatalog(); + + assertEquals(4, catalog.getAvailablePlugins().size()); + assertTrue(catalog.getAvailablePlugins().contains("Uppercase: Converts Texts to UPPERCASE")); + } + + @Test + void createsFreshPluginFromDisplayLabel() { + var catalog = new PluginCatalog(); + + var first = catalog.createPlugin("Uppercase: Converts Texts to UPPERCASE"); + var second = catalog.createPlugin("Uppercase"); + + assertEquals("Uppercase", first.getName()); + assertNotSame(first, second); + } + + @Test + void rejectsUnknownPlugin() { + var catalog = new PluginCatalog(); + + var exception = + assertThrows(IllegalArgumentException.class, () -> 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..c7f690c042f8 --- /dev/null +++ b/microkernel-architecture/src/test/java/com/iluwatar/microkernel/plugins/PluginImplementationsTest.java @@ -0,0 +1,130 @@ +/* + * 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.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; +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")); + } + + @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; + } + } +} 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()); + } +} 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