{"id":19762596,"url":"https://github.com/pnavais/state-machine","last_synced_at":"2025-04-30T14:31:02.380Z","repository":{"id":43668427,"uuid":"139744472","full_name":"pnavais/state-machine","owner":"pnavais","description":"Generic State Machine Java implementation. Zero-Dependency.","archived":false,"fork":false,"pushed_at":"2024-07-22T16:38:23.000Z","size":763,"stargazers_count":16,"open_issues_count":1,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-07-22T20:12:08.777Z","etag":null,"topics":["java-8","state-machines","zero-dependency"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pnavais.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-07-04T16:21:34.000Z","updated_at":"2024-05-13T10:31:12.000Z","dependencies_parsed_at":"2024-07-22T20:06:47.472Z","dependency_job_id":null,"html_url":"https://github.com/pnavais/state-machine","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pnavais%2Fstate-machine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pnavais%2Fstate-machine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pnavais%2Fstate-machine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pnavais%2Fstate-machine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pnavais","download_url":"https://codeload.github.com/pnavais/state-machine/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224214145,"owners_count":17274524,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["java-8","state-machines","zero-dependency"],"created_at":"2024-11-12T04:05:20.147Z","updated_at":"2024-11-12T04:05:20.826Z","avatar_url":"https://github.com/pnavais.png","language":"Java","readme":"\u003cp align=\"center\"\u003e\r\n    \u003cimg src=\"images/logo.png\"/\u003e\r\n\u003c/p\u003e\r\n\r\n\u003cp align=\"center\"\u003e\r\n    \u003ca href=\"https://github.com/pnavais/state-machine/actions?query=workflows/maven-publish.yml\"\u003e\r\n        \u003cimg src=\"https://github.com/pnavais/state-machine/actions/workflows/maven-publish.yml/badge.svg\"\r\n             alt=\"Build Status\"/\u003e\r\n    \u003c/a\u003e\r\n    \u003ca href='https://coveralls.io/github/pnavais/state-machine?branch=master'\u003e\r\n\t    \u003cimg src='https://coveralls.io/repos/github/pnavais/state-machine/badge.svg?branch=master'\r\n\t\t alt='Coverage Status'/\u003e\r\n    \u003c/a\u003e\r\n    \u003ca href=\"https://github.com/pnavais/state-machine/blob/master/LICENSE\"\u003e\r\n\t\u003cimg src=\"https://img.shields.io/github/license/pnavais/state-machine\"\r\n\t     alt=\"License\"/\u003e\r\n    \u003c/a\u003e     \r\n    \u003ca href=\"https://sonarcloud.io/summary/new_code?id=pnavais_state-machine\"\u003e    \r\n        \u003cimg src=\"https://sonarcloud.io/api/project_badges/measure?project=pnavais_state-machine\u0026metric=alert_status\"\r\n             alt=\"Quality Gate\"/\u003e\r\n    \u003c/a\u003e\r\n    \u003ca href=\"https://maven-badges.herokuapp.com/maven-central/com.github.pnavais/state-machine\"\u003e\u003cimg src=\"https://img.shields.io/maven-central/v/com.github.pnavais/state-machine\"\r\n             alt=\"Maven Central\"/\u003e\u003c/a\u003e\r\n\t\r\n\u003c/p\u003e\r\n\r\n\u003cp align=\"center\"\u003e\u003csup\u003e\u003cstrong\u003eGeneric State Machine implementation for Java 8+\u003c/strong\u003e\u003c/sup\u003e\u003c/p\u003e\r\n\r\n## Maven Repository\r\n\r\nYou can pull the library from central maven repository, just add these to your pom.xml file:\r\n```xml\r\n\u003cdependency\u003e\r\n  \u003cgroupId\u003ecom.github.pnavais\u003c/groupId\u003e\r\n  \u003cartifactId\u003estate-machine\u003c/artifactId\u003e\r\n  \u003cversion\u003e1.2.0\u003c/version\u003e\r\n\u003c/dependency\u003e\r\n```\r\n\r\n## Basic usage\r\n\r\n```java\r\nStateMachine stateMachine = StateMachine.newBuilder()\r\n                .from(\"A\").to(\"B\").on(\"1\")\r\n                .from(\"B\").to(\"C\").on(\"2\")                \r\n                .build();\r\n ```\r\n \r\nCreates a new State Machine as per the following diagram : \r\n\r\n![alt text](images/simple_graph.png \"Simple graph diagram\")\r\n\r\nWhen using the builder, the State Machine is automatically initialized using as current state the first node added (i.e \"A\" in the previous example).\r\n\r\nA transition can be specified without a named message : \r\n\r\n```java\r\nStateMachine stateMachine = StateMachine.newBuilder()\r\n                .from(\"A\").to(\"B\").build();\r\n ```\r\n\r\nwhich is a shorthand equivalent to : \r\n```java\r\nStateMachine stateMachine = StateMachine.newBuilder()\r\n                .from(\"A\").to(\"B\").on(Messages.EMPTY).build();\r\n```\r\n \r\nTransitions for any message can be specified using : \r\n ```java\r\nStateMachine stateMachine = StateMachine.newBuilder()\r\n                .from(\"A\").to(\"B\").on(Messages.ANY).build();\r\n ```\r\n\r\n### Traversal\r\n\r\nOnce initialized, the State Machine can be traversed by sending named messages :\r\n\r\n```java\r\n// A --- 1 ---\u003e B --- 2 ---\u003e C\r\nState current = stateMachine.send(\"1\").send(\"2\").getCurrent(); \r\nSystem.out.println(current.getName()); // --\u003e \"C\"\r\n```\r\n\r\nor empty messages : \r\n\r\n```java\r\n// A ---\u003e B\r\nState current = stateMachine.next().getCurrent(); \r\nSystem.out.println(current.getName()); // --\u003e \"B\"\r\n```\r\n\r\nAdditionally wildcard messages can also be sent (if transitions supporting wildcards were added) : \r\n\r\n ```java\r\nStateMachine stateMachine = StateMachine.newBuilder()\r\n                .from(\"A\").to(\"B\").on(Messages.ANY)\r\n                .from(\"A\").to(\"C\").on(\"3\").build();\r\n ```\r\n\r\nWildcard messages are used as fallback if the state does not support a direct transition for the given message i.e. : \r\n\r\n```java\r\nstateMachine.getNext(\"3\"); // --\u003e C\r\nstateMachine.init();       // --\u003e A again\r\nstateMachine.getNext(\"4\")  // --\u003e B\r\n```\r\n\r\nIn case the current state does not support the message sent, the latter will be silently ignored and thus no transition will be made.\r\nBe aware that an **empty message is not similar to a wildcard message** (i.e. Messages.EMPTY != Messages.ANY) and thus a transition defined with no message is only triggered by an empty message.\r\n\r\nThe current state can be set to any existing state at any time : \r\n```java\r\nstateMachine.setCurrent(\"A\");\r\n// OR\r\nstateMachine.setCurrent(new State(\"A\"));\r\n\r\nState next = stateMachine.getNext(\"3\"); // --\u003e C\r\n```\r\n\r\nIn case the given state is not recognized a NullStateException is raised.\r\n \r\n ## Advanced usage\r\n \r\n ### Importing from files (\u003e=1.1.0)\r\n \r\n State machines can be created by loading a YAML specification file as shown in the following example.\r\n Consider this simplistic state machine YAML specification representing some docker commands : \r\n \r\n ```yml\r\nstates:\r\n\t- state:\r\n\t\tname: \"Initial\"\r\n\t- state:\r\n\t\tname: \"Created\"\r\n\t\tcurrent: \"true\"\r\n\t\tproperties: \r\n\t\t\tcolor: \"#7B8DBD\"\r\n\t- state:\r\n\t\tname: \"Running\"     \r\n\t\tproperties:\r\n\t\t\tstyle: \"filled\"\r\n\t\t\tfillcolor: \"#95AF82\"\r\n\t- state:\r\n\t\tname: \"Stopped\"\r\n\t\tproperties:\r\n\t\t\tstyle: \"filled\"\r\n\t\t\tfillcolor: \"#B19186\"\r\n\t- state:\r\n\t\tname: \"Paused\"\r\n\t\tproperties:\r\n\t\t\tstyle: \"filled\"\r\n\t\t\tfillcolor: \"#D3C09F\"\r\ntransitions:\r\n\t- transition:\r\n\t\tsource:  \"Initial\"\r\n\t\ttarget:  \"Created\"\r\n\t\tmessage: \"docker create\"\r\n\t- transition:\r\n\t\tsource:  \"Created\"\r\n\t\ttarget:  \"Running\"\r\n\t\tmessage: \"docker start\"\r\n\t- transition:\r\n\t\tsource:  \"Running\"\r\n\t\ttarget:  \"Stopped\"\r\n\t\tmessage: \"docker stop\"\r\n\t- transition:\r\n\t\tsource:  \"Stopped\"\r\n\t\ttarget:  \"Running\"\r\n\t\tmessage: \"docker start\"\r\n\t- transition:\r\n\t\tsource:  \"Running\"\r\n\t\ttarget:  \"Paused\"\r\n\t\tmessage: \"docker pause\"\r\n\t- transition:\r\n\t\tsource:  \"Paused\"\r\n\t\ttarget:  \"Running\"\r\n\t\tmessage: \"docker unpause\"\r\n ```\r\n  \r\n  This YAML file can be later imported with :\r\n \r\n ```java\r\n StateMachine dockerMachine = YAMLImporter.builder().build().parseFile(\"docker-machine.yml\");\r\n ```\r\n \r\n Which eventually leads to the following graph : \r\n \r\n ![alt text](images/docker_graph.png \"Docker state machine after import\") \r\n   \r\n ### Initialization using State Transitions\r\n \r\n State transitions can be used directly when building the machine :\r\n ```java\r\n StateMachine stateMachine = StateMachine.newBuilder().add(new StateTransition(\"A\", \"1\", \"B\")).build();\r\n ```\r\n \r\n  ### Initialization without the Builder\r\n  \r\n  The State Machine can also be initialized directly without the builder fluent language this way : \r\n  \r\n  ```java\r\n  StateMachine stateMachine = new StateMachine();\r\n  \r\n  stateMachine.add(new StateTransition(\"a\", \"0.2\", \"b\"));\r\n  stateMachine.add(new StateTransition(\"a\", \"0.4\", \"c\"));\r\n  stateMachine.add(new StateTransition(\"c\", \"0.6\", \"e\"));\r\n  stateMachine.add(new StateTransition(\"e\", \"0.1\", \"e\"));\r\n  stateMachine.add(new StateTransition(\"e\", \"0.7\", \"b\"));  \r\n  ```\r\n  \r\n  Which leads to the following diagram : \r\n  \r\n  ![alt text](images/manual_graph.png \"Manual State machine creation graph diagram\")\r\n  \r\nPlease notice that the current state of the machine after manual creation must be specified manually \r\n```java\r\nstateMachine.init();          // --\u003e Initializes to the first state added to the machine (i.e. a)\r\n// OR\r\nstateMachine.setCurrent(\"b\"); // --\u003e Sets the current state explicitly\r\n```\r\n  \r\n ### Self loops\r\n \r\n Transitions to the same state can be specified this way : \r\n \r\n ```java\r\nStateMachine stateMachine = StateMachine.newBuilder()\r\n                .from(\"A\").to(\"B\").on(\"1\")\r\n                .from(\"B\").to(\"C\").on(\"3\")\r\n                .selfLoop(\"B\").on(\"2\")\r\n                .build();\r\n ```\r\n \r\n Which is equivalent to the following state machine diagram : \r\n \r\n ![alt text](images/graph_with_loops.png \"Graph with loops\")\r\n \r\n \r\n### Initialization using custom States\r\n\r\n```java\r\nState initialState = new State(\"A\");\r\nStateMachine stateMachine = StateMachine.newBuilder().from(initialState).to(\"B\").build();\r\n```\r\nWhen adding states to the machine, the name is used to verify if the state is already in place. In that case no additional state is added but rather merged to the existing one (See [Merging states](#Merging-states) section for more information).\r\n\r\n \r\n ### Final states\r\n \r\n States can be flagged as final in order to avoid potential transitions from them : \r\n \r\n ```java\r\nStateMachine stateMachine = StateMachine.newBuilder()\r\n                .from(\"A\").to(\"B\")\r\n                .from(\"B\").to(State.from(\"C\").isFinal(true).build())\r\n                .build();\r\n ```\r\n \r\n In case a transition is later added from a final state an IllegalTransitionException is raised.\r\n  \r\n \r\n ### Message filtering\r\n \r\nCustom handlers can be specified globally or message-scoped to intercept transitions occurring in the State Machine which are in turn triggered by incoming messages. These handlers can be specified at either departure or arrival of the transition. \r\n \r\n See the following examples to have a better understanding of the concept.\r\n   \r\n #### Global filters\r\n \r\n Just add a ```\"leaving\"``` or ```\"arriving\"``` clause to the builder specifying the handler to be executed on departure/arrival to the states involved in the current transition.\r\n \r\n ```java\r\n // Adds a global handler to filter any depature from state A\r\n  StateMachine stateMachine = StateMachine.newBuilder()\r\n                .add(new StateTransition(\"A\", \"1\",\"B\"))\r\n                .add(new StateTransition(\"A\", \"2\",\"C\"))\r\n                .leaving(\"A\").execute(context -\u003e {\r\n                    messages.add(String.format(\"Departing from [%s] to [%s] on [%s]\", context.getSource(), context.getTarget(),context.getMessage()));\r\n                    return Status.PROCEED;\r\n                }).build();\r\n ```\r\n \r\nIn this case, the lambda function specified when leaving state A will be executed for any message received. The transition can be either accepted/rejected depending on the supplied Status (predefined PROCEED/ABORT or custom with a given validity status flag).\r\n \r\n#### Message-scoped filters\r\n \r\nThe handlers can be also specified on a per message basis as described below :\r\n\r\n```java\r\nStateMachine stateMachine = StateMachine.newBuilder()\r\n                .from(\"A\").to(\"B\").on(\"1\").arriving(context -\u003e {\r\n                    return doSomeProcessing(); // Do wathever you want and return a Status\r\n                })\r\n```\r\n\r\nIn this case, the lambda function will only be executed when the \"1\" message is sent for a transition from A to B.\r\n \r\n ### Custom messages\r\n \r\nState Machine supports by default an special implementation of the ```Message``` interface i.e. ```StringMessage``` which only contains a message identifier as payload but any special Message can be specified.\r\n\r\nThe following example specifies a Message with a custom payload : \r\n\r\n```java\r\nAtomicInteger counter = new AtomicInteger(100);\r\n\r\nMessage customMessage = new Message() {\r\n    @Override\r\n    public UUID getMessageId() {\r\n        return UUID.randomUUID();\r\n    }\r\n\r\n    @Override\r\n    public Payload getPayload() {\r\n        return () -\u003e counter;\r\n    }\r\n};\r\n\r\nStateMachine stateMachine = StateMachine.newBuilder()\r\n        .from(\"A\").to(\"B\").on(customMessage)\r\n        .from(\"A\").to(\"C\")\r\n        .leaving(\"A\").execute(context -\u003e {\r\n            System.out.println(\"Counter \u003e\u003e \"+context.getMessage().getPayload().get());\r\n            return Status.PROCEED;\r\n        }).build();\r\n\r\nstateMachine.init();\r\nstateMachine.send(message).getCurrent(); // Counter \u003e\u003e 100 (The integer payload) , current state -\u003e B\r\n\r\nstateMachine.init();\r\nSystem.out.println(stateMachine.next().getCurrent()); // Counter \u003e\u003e _ (Empty payload) , current state -\u003e C\r\n```\r\n \r\n ### Custom properties\r\n \r\nState instances can optionally contain any arbitrary String property attached to them (this is specially useful when exporting the state machine to an output format).\r\n\r\n```java\r\nState state = new State(\"A\");\r\nstate.addProperty(\"prop\", \"value\"); // To add the property with the given value\r\nstate.removeProperty(\"prop\");       // To remove it\r\n```\r\n\r\n### Pruning orphan states\r\n\r\nIf for some reason an state cannot be reached by any transition, it is considered orphan.Taking into account the previous statement, be aware that a state only reacheable through a self loop is not deemed orphan.\r\n\r\nConsider the following state machine : \r\n```java\r\nStateMachine machine = StateMachine.newBuilder()\r\n                .from(\"A\").to(\"B\").on(\"1\")\r\n                .from(\"A\").to(\"C\").on(\"2\")\r\n                .from(\"B\").to(\"D\").on(\"3\").build();\r\n```\r\n\r\nWhich is initially represented by :\r\n\r\n![alt text](images/orphan_graph.png \"Graph before orphan states\")\r\n \r\nWhen removing state B, state D is considered orphan : \r\n```java\r\nmachine.remove(\"B\"); // --\u003e State D not reachable\r\n```\r\n\r\n![alt text](images/orphan_graph1.png \"Graph with orphan states\")\r\n\r\n To automatically remove orphan states do the following : \r\n \r\n ```java\r\n machine.prune();\r\n ```\r\n \r\n leading to : \r\n \r\n![alt text](images/orphan_graph2.png \"Graph after pruning\")\r\n  \r\n ### Merging states\r\n \r\nAs already mentioned previously, in case a new state to be added to the State Machine already exists, the information of both states (existing and new) is merged automatically. This implies preserving the final state value and copying/overriding properties and message filters (if any).\r\nThe behaviour of the merge functionality can be overriden or implemented through the Mergeable interface.\r\n\r\n ### Exporting to GraphViz DOT language format\r\n \r\n A very basic DOT exporter is also provided allowing to export a given State Machine to the DOT language : \r\n \r\n```java\r\nStateMachine stateMachine = StateMachine.newBuilder()\r\n                .from(\"A\").to(\"B\").on(\"3\")\r\n                .selfLoop(\"C\").on(\"3\")\r\n                .from(\"B\").to(\"C\").on(\"4\")\r\n                .selfLoop(\"A\").on(Messages.ANY)\r\n                .from(\"B\").to(State.from(\"D\").isFinal(true).build())\r\n                .build();\r\n\r\nDOTExporter.builder().build().exportToFile(stateMachine, \"graph.gv\");\r\n```\r\n\r\nWhich eventually can be later processed by the DOT tool to produce an image :\r\n\r\n```\r\ndot -Tpng graph.gv -o graph.png\r\n```\r\n\r\n ![alt text](images/exported_graph.png \"Exported DOT graph\")\r\n \r\n \u003cdiv\u003e\u003csup\u003eIcon made by \u003ca href=\"https://www.flaticon.com/authors/smashicons\" title=\"Smashicons\"\u003eSmashicons\u003c/a\u003e from \u003ca href=\"http://www.flaticon.com\" title=\"Flaticon\"\u003ewww.flaticon.com\u003c/a\u003e\u003c/sup\u003e\u003c/div\u003e\r\n\r\n \r\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpnavais%2Fstate-machine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpnavais%2Fstate-machine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpnavais%2Fstate-machine/lists"}