├── LICENSE ├── .gitignore ├── README.md ├── src ├── test │ └── java │ │ └── io │ │ └── github │ │ └── the_pocket │ │ └── PocketFlowTest.java └── main │ └── java │ └── io │ └── github │ └── the_pocket │ └── PocketFlow.java └── pom.xml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Zachary Huang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class files 2 | *.class 3 | 4 | # Log files 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs 23 | hs_err_pid* 24 | replay_pid* 25 | 26 | # Maven 27 | target/ 28 | pom.xml.tag 29 | pom.xml.releaseBackup 30 | pom.xml.versionsBackup 31 | pom.xml.next 32 | release.properties 33 | dependency-reduced-pom.xml 34 | buildNumber.properties 35 | .mvn/timing.properties 36 | .mvn/wrapper/maven-wrapper.jar 37 | 38 | # Gradle 39 | .gradle/ 40 | build/ 41 | !gradle/wrapper/gradle-wrapper.jar 42 | 43 | # IDE-specific files 44 | # Eclipse 45 | .settings/ 46 | .classpath 47 | .project 48 | .metadata/ 49 | 50 | # IntelliJ IDEA 51 | .idea/ 52 | *.iws 53 | *.iml 54 | *.ipr 55 | out/ 56 | 57 | # NetBeans 58 | /nbproject/private/ 59 | /nbbuild/ 60 | /dist/ 61 | /nbdist/ 62 | /.nb-gradle/ 63 | 64 | # VS Code 65 | .vscode/ 66 | .code-workspace 67 | 68 | # OS-specific files 69 | .DS_Store 70 | .DS_Store? 71 | ._* 72 | .Spotlight-V100 73 | .Trashes 74 | ehthumbs.db 75 | Thumbs.db 76 | 77 | test.ipynb 78 | .pytest_cache/ 79 | settings.xml 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PocketFlow Java 2 | 3 | A minimalist LLM framework, ported from Python to Java. 4 | 5 | ## Overview 6 | 7 | PocketFlow Java is a direct port of the original [Python PocketFlow](https://github.com/The-Pocket/PocketFlow) framework. It provides a lightweight, flexible system for building and executing LLM-based workflows through a simple node-based architecture. 8 | 9 | > **Note:** This is an initial implementation that currently does not support asynchronous operations. Community contributors are welcome to help enhance and maintain this project. 10 | 11 | ## Installation 12 | 13 | ### Maven 14 | 15 | Add the following dependency to your `pom.xml`: 16 | 17 | ```xml 18 | 19 | io.github.the-pocket 20 | PocketFlow 21 | 1.0.0 22 | 23 | ``` 24 | 25 | ### Gradle 26 | 27 | **Groovy DSL (`build.gradle`):** 28 | ```groovy 29 | dependencies { 30 | implementation 'io.github.the-pocket:PocketFlow:1.0.0' 31 | } 32 | ``` 33 | 34 | **Kotlin DSL (`build.gradle.kts`):** 35 | ```kotlin 36 | dependencies { 37 | implementation("io.github.the-pocket:PocketFlow:1.0.0") 38 | } 39 | ``` 40 | 41 | ## Usage 42 | 43 | Here's a simple example of how to use PocketFlow Java in your application: 44 | 45 | ```java 46 | import io.github.the_pocket.PocketFlow; 47 | import io.github.the_pocket.PocketFlow.*; 48 | import java.util.HashMap; 49 | import java.util.Map; 50 | 51 | public class MyWorkflowApp { 52 | 53 | // Define a custom start node 54 | static class MyStartNode extends Node { 55 | @Override 56 | public String exec(Void prepResult) { 57 | System.out.println("Starting workflow..."); 58 | return "started"; 59 | } 60 | } 61 | 62 | // Define a custom end node 63 | static class MyEndNode extends Node { 64 | @Override 65 | public String prep(Map ctx) { 66 | return "Preparing to end workflow"; 67 | } 68 | 69 | @Override 70 | public Void exec(String prepResult) { 71 | System.out.println("Ending workflow with: " + prepResult); 72 | return null; 73 | } 74 | } 75 | 76 | public static void main(String[] args) { 77 | // Create instances of your nodes 78 | MyStartNode startNode = new MyStartNode(); 79 | MyEndNode endNode = new MyEndNode(); 80 | 81 | // Connect the nodes 82 | startNode.next(endNode, "started"); 83 | 84 | // Create a flow with the start node 85 | Flow flow = new Flow<>(startNode); 86 | 87 | // Create a context and run the flow 88 | Map context = new HashMap<>(); 89 | System.out.println("Executing workflow..."); 90 | flow.run(context); 91 | System.out.println("Workflow completed successfully."); 92 | } 93 | } 94 | ``` 95 | 96 | ## Development 97 | 98 | ### Building the Project 99 | 100 | ```bash 101 | mvn compile 102 | ``` 103 | 104 | ### Running Tests 105 | 106 | ```bash 107 | mvn test 108 | ``` 109 | 110 | ## Contributing 111 | 112 | Contributions are welcome! We're particularly looking for volunteers to: 113 | 114 | 1. Implement asynchronous operation support 115 | 2. Add comprehensive test coverage 116 | 3. Improve documentation and provide examples 117 | 118 | 119 | Please feel free to submit pull requests or open issues for discussion. 120 | 121 | ## License 122 | 123 | [MIT License](LICENSE) -------------------------------------------------------------------------------- /src/test/java/io/github/the_pocket/PocketFlowTest.java: -------------------------------------------------------------------------------- 1 | package io.github.the_pocket; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import io.github.the_pocket.PocketFlow.*; 11 | 12 | class PocketFlowTest { 13 | 14 | // --- Test Node Implementations --- 15 | 16 | static class SetNumberNode extends Node { 17 | private final int number; 18 | public SetNumberNode(int number) { this.number = number; } 19 | @Override public Integer exec(Void prepResult) { return number * (Integer) params.getOrDefault("multiplier", 1); } 20 | @Override public String post(Map ctx, Void p, Integer e) { 21 | ctx.put("currentValue", e); 22 | return e > 20 ? "over_20" : "default"; 23 | } 24 | } 25 | 26 | static class AddNumberNode extends Node { 27 | private final int numberToAdd; 28 | public AddNumberNode(int numberToAdd) { this.numberToAdd = numberToAdd;} 29 | @Override public Integer prep(Map ctx) { return (Integer)ctx.get("currentValue"); } 30 | @Override public Integer exec(Integer currentValue) { return currentValue + numberToAdd; } 31 | @Override public String post(Map ctx, Integer p, Integer e) { 32 | ctx.put("currentValue", e); 33 | return "added"; 34 | } 35 | } 36 | 37 | static class ResultCaptureNode extends Node { 38 | @Override public Integer prep(Map ctx) { return (Integer)ctx.getOrDefault("currentValue", -999); } 39 | @Override public Void exec(Integer prepResult) { 40 | params.put("capturedValue", prepResult); 41 | return null; 42 | } 43 | } 44 | 45 | static class SimpleLogNode extends Node { 46 | @Override public String exec(Void prepResult) { 47 | String message = "SimpleLogNode executed with multiplier: " + params.get("multiplier"); 48 | return message; 49 | } 50 | @Override public String post(Map ctx, Void p, String e) { 51 | ctx.put("last_message_from_batch_" + params.get("multiplier"), e); 52 | return null; 53 | } 54 | } 55 | 56 | // --- Test Methods --- 57 | 58 | @Test 59 | void testSimpleLinearFlow() { 60 | SetNumberNode start = new SetNumberNode(10); 61 | AddNumberNode addSync = new AddNumberNode(5); 62 | ResultCaptureNode captureFinal = new ResultCaptureNode(); 63 | 64 | start.next(addSync, "default").next(captureFinal, "added"); 65 | 66 | Flow syncFlow = new Flow(start); 67 | Map sharedContext = new HashMap<>(); 68 | String lastAction = syncFlow.run(sharedContext); 69 | 70 | assertNull(lastAction); 71 | assertEquals(15, sharedContext.get("currentValue")); 72 | assertEquals(15, captureFinal.params.get("capturedValue")); 73 | } 74 | 75 | @Test 76 | void testBranchingFlow() { 77 | SetNumberNode start = new SetNumberNode(10); 78 | AddNumberNode addSync = new AddNumberNode(5); 79 | ResultCaptureNode captureFinalDefault = new ResultCaptureNode(); 80 | ResultCaptureNode captureFinalOver20 = new ResultCaptureNode(); 81 | 82 | start.next(addSync, "default").next(captureFinalDefault, "added"); 83 | start.next(captureFinalOver20, "over_20"); 84 | 85 | Flow syncFlow = new Flow(start); 86 | Map sharedContext = new HashMap<>(); 87 | 88 | syncFlow.setParams(Map.of("multiplier", 3)); 89 | String lastAction = syncFlow.run(sharedContext); 90 | 91 | assertNull(lastAction); 92 | assertEquals(30, sharedContext.get("currentValue")); 93 | assertEquals(30, captureFinalOver20.params.get("capturedValue")); 94 | assertNull(captureFinalDefault.params.get("capturedValue")); 95 | } 96 | 97 | @Test 98 | void testBatchFlowExecution() { 99 | class MyTestBatchFlow extends BatchFlow { 100 | public MyTestBatchFlow(BaseNode startNode) { super(startNode); } 101 | @Override 102 | public List> prepBatch(Map sharedContext) { 103 | return List.of( Map.of("multiplier", 2), Map.of("multiplier", 4) ); 104 | } 105 | @Override 106 | public String postBatch(Map sharedContext, List> batchPrepResult) { 107 | sharedContext.put("postBatchCalled", true); 108 | return "batch_complete"; 109 | } 110 | } 111 | 112 | MyTestBatchFlow batchFlow = new MyTestBatchFlow(new SimpleLogNode()); 113 | Map batchContext = new HashMap<>(); 114 | String result = batchFlow.run(batchContext); 115 | 116 | assertEquals("batch_complete", result); 117 | assertTrue((Boolean)batchContext.get("postBatchCalled")); 118 | assertEquals("SimpleLogNode executed with multiplier: 2", batchContext.get("last_message_from_batch_2")); 119 | assertEquals("SimpleLogNode executed with multiplier: 4", batchContext.get("last_message_from_batch_4")); 120 | } 121 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | io.github.the-pocket 9 | PocketFlow 10 | 1.0.0 11 | jar 12 | 13 | 14 | PocketFlow 15 | Pocket Flow: A minimalist LLM framework. Let Agents build Agents! 16 | https://github.com/The-Pocket/PocketFlow-Java 17 | 18 | 19 | 20 | 21 | MIT License 22 | http://www.opensource.org/licenses/mit-license.php 23 | repo 24 | 25 | 26 | 27 | 28 | 29 | 30 | zachary62 31 | Zachary Huang 32 | https://github.com/zachary62 33 | 34 | 35 | 36 | 37 | 38 | scm:git:git://github.com/The-Pocket/PocketFlow-Java.git 39 | scm:git:ssh://git@github.com/The-Pocket/PocketFlow-Java.git 40 | https://github.com/The-Pocket/PocketFlow-Java/tree/main 41 | HEAD 42 | 43 | 44 | 45 | 46 | UTF-8 47 | 11 48 | 11 49 | 5.9.2 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | org.junit.jupiter 58 | junit-jupiter-api 59 | ${junit.jupiter.version} 60 | test 61 | 62 | 63 | org.junit.jupiter 64 | junit-jupiter-engine 65 | ${junit.jupiter.version} 66 | test 67 | 68 | 69 | 70 | 71 | 72 | 73 | ossrh 74 | https://s01.oss.sonatype.org/content/repositories/snapshots 75 | 76 | 77 | ossrh 78 | https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | org.apache.maven.plugins 88 | maven-compiler-plugin 89 | 3.11.0 90 | 91 | ${maven.compiler.source} 92 | ${maven.compiler.target} 93 | 94 | 95 | 96 | 97 | org.apache.maven.plugins 98 | maven-surefire-plugin 99 | 3.0.0 100 | 101 | 102 | 103 | org.apache.maven.plugins 104 | maven-source-plugin 105 | 3.2.1 106 | 107 | 108 | attach-sources 109 | 110 | jar-no-fork 111 | 112 | 113 | 114 | 115 | 116 | 117 | org.apache.maven.plugins 118 | maven-javadoc-plugin 119 | 3.5.0 120 | 121 | ${maven.compiler.source} 122 | 123 | 124 | 125 | attach-javadocs 126 | 127 | jar 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-gpg-plugin 137 | 3.1.0 138 | 139 | 140 | sign-artifacts 141 | verify 142 | 143 | sign 144 | 145 | 146 | 147 | 148 | 149 | 150 | --pinentry-mode 151 | loopback 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | org.sonatype.central 161 | central-publishing-maven-plugin 162 | 0.7.0 163 | true 164 | 165 | 166 | central 167 | 168 | true 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /src/main/java/io/github/the_pocket/PocketFlow.java: -------------------------------------------------------------------------------- 1 | package io.github.the_pocket; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Objects; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | /** 12 | * PocketFlow - A simple, synchronous workflow library for Java in a single file. 13 | */ 14 | public final class PocketFlow { 15 | 16 | private PocketFlow() {} 17 | 18 | private static void logWarn(String message) { 19 | System.err.println("WARN: PocketFlow - " + message); 20 | } 21 | 22 | public static class PocketFlowException extends RuntimeException { 23 | public PocketFlowException(String message) { super(message); } 24 | public PocketFlowException(String message, Throwable cause) { super(message, cause); } 25 | } 26 | 27 | /** 28 | * Base class for all nodes in the workflow. 29 | * 30 | * @param

Type of the result from the prep phase. 31 | * @param Type of the result from the exec phase. 32 | * The return type of post/run is always String (or null for default action). 33 | */ 34 | public static abstract class BaseNode { 35 | protected Map params = new HashMap<>(); 36 | protected final Map> successors = new HashMap<>(); 37 | public static final String DEFAULT_ACTION = "default"; 38 | 39 | public BaseNode setParams(Map params) { 40 | this.params = params != null ? new HashMap<>(params) : new HashMap<>(); 41 | return this; 42 | } 43 | 44 | public BaseNode next(BaseNode node) { 45 | return next(node, DEFAULT_ACTION); 46 | } 47 | 48 | public BaseNode next(BaseNode node, String action) { 49 | Objects.requireNonNull(node, "Successor node cannot be null"); 50 | Objects.requireNonNull(action, "Action cannot be null"); 51 | if (this.successors.containsKey(action)) { 52 | logWarn("Overwriting successor for action '" + action + "' in node " + this.getClass().getSimpleName()); 53 | } 54 | this.successors.put(action, node); 55 | return node; 56 | } 57 | 58 | public P prep(Map sharedContext) { return null; } 59 | public abstract E exec(P prepResult); 60 | /** Post method MUST return a String action, or null for the default action. */ 61 | public String post(Map sharedContext, P prepResult, E execResult) { return null; } 62 | 63 | protected E internalExec(P prepResult) { return exec(prepResult); } 64 | 65 | protected String internalRun(Map sharedContext) { 66 | P prepRes = prep(sharedContext); 67 | E execRes = internalExec(prepRes); 68 | return post(sharedContext, prepRes, execRes); 69 | } 70 | 71 | public String run(Map sharedContext) { 72 | if (!successors.isEmpty()) { 73 | logWarn("Node " + getClass().getSimpleName() + " has successors, but run() was called. Successors won't be executed. Use Flow."); 74 | } 75 | return internalRun(sharedContext); 76 | } 77 | 78 | protected BaseNode getNextNode(String action) { 79 | String actionKey = (action != null) ? action : DEFAULT_ACTION; 80 | BaseNode nextNode = successors.get(actionKey); 81 | if (nextNode == null && !successors.isEmpty() && !successors.containsKey(actionKey)) { 82 | logWarn("Flow might end: Action '" + actionKey + "' not found in successors " 83 | + successors.keySet() + " of node " + this.getClass().getSimpleName()); 84 | } 85 | return nextNode; 86 | } 87 | } 88 | 89 | /** 90 | * A synchronous node with built-in retry capabilities. 91 | */ 92 | public static abstract class Node extends BaseNode { 93 | protected final int maxRetries; 94 | protected final long waitMillis; 95 | protected int currentRetry = 0; 96 | 97 | public Node() { this(1, 0); } 98 | public Node(int maxRetries, long waitMillis) { 99 | if (maxRetries < 1) throw new IllegalArgumentException("maxRetries must be at least 1"); 100 | if (waitMillis < 0) throw new IllegalArgumentException("waitMillis cannot be negative"); 101 | this.maxRetries = maxRetries; 102 | this.waitMillis = waitMillis; 103 | } 104 | 105 | public E execFallback(P prepResult, Exception lastException) throws Exception { throw lastException; } 106 | 107 | @Override 108 | protected E internalExec(P prepResult) { 109 | Exception lastException = null; 110 | for (currentRetry = 0; currentRetry < maxRetries; currentRetry++) { 111 | try { return exec(prepResult); } 112 | catch (Exception e) { 113 | lastException = e; 114 | if (currentRetry < maxRetries - 1 && waitMillis > 0) { 115 | try { TimeUnit.MILLISECONDS.sleep(waitMillis); } 116 | catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new PocketFlowException("Thread interrupted during retry wait", ie); } 117 | } 118 | } 119 | } 120 | try { 121 | if (lastException == null) throw new PocketFlowException("Execution failed, but no exception was captured."); 122 | return execFallback(prepResult, lastException); 123 | } catch (Exception fallbackException) { 124 | if (lastException != null && fallbackException != lastException) fallbackException.addSuppressed(lastException); 125 | if (fallbackException instanceof RuntimeException) throw (RuntimeException) fallbackException; 126 | else throw new PocketFlowException("Fallback execution failed", fallbackException); 127 | } 128 | } 129 | } 130 | 131 | /** 132 | * A synchronous node that processes a list of items individually. 133 | */ 134 | public static abstract class BatchNode extends Node, List> { 135 | public BatchNode() { super(); } 136 | public BatchNode(int maxRetries, long waitMillis) { super(maxRetries, waitMillis); } 137 | 138 | public abstract OUT_ITEM execItem(IN_ITEM item); 139 | public OUT_ITEM execItemFallback(IN_ITEM item, Exception lastException) throws Exception { throw lastException; } 140 | 141 | @Override 142 | protected List internalExec(List batchPrepResult) { 143 | if (batchPrepResult == null || batchPrepResult.isEmpty()) return Collections.emptyList(); 144 | List results = new ArrayList<>(batchPrepResult.size()); 145 | for (IN_ITEM item : batchPrepResult) { 146 | Exception lastItemException = null; 147 | OUT_ITEM itemResult = null; 148 | boolean itemSuccess = false; 149 | for (currentRetry = 0; currentRetry < maxRetries; currentRetry++) { 150 | try { itemResult = execItem(item); itemSuccess = true; break; } 151 | catch (Exception e) { 152 | lastItemException = e; 153 | if (currentRetry < maxRetries - 1 && waitMillis > 0) { 154 | try { TimeUnit.MILLISECONDS.sleep(waitMillis); } 155 | catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new PocketFlowException("Interrupted batch retry wait: " + item, ie); } 156 | } 157 | } 158 | } 159 | if (!itemSuccess) { 160 | try { 161 | if (lastItemException == null) throw new PocketFlowException("Item exec failed without exception: " + item); 162 | itemResult = execItemFallback(item, lastItemException); 163 | } catch (Exception fallbackException) { 164 | if (lastItemException != null && fallbackException != lastItemException) fallbackException.addSuppressed(lastItemException); 165 | if (fallbackException instanceof RuntimeException) throw (RuntimeException) fallbackException; 166 | else throw new PocketFlowException("Item fallback failed: " + item, fallbackException); 167 | } 168 | } 169 | results.add(itemResult); 170 | } 171 | return results; 172 | } 173 | } 174 | 175 | /** 176 | * Orchestrates the execution of a sequence of connected nodes. 177 | */ 178 | public static class Flow extends BaseNode { 179 | protected BaseNode startNode; 180 | 181 | public Flow() { this(null); } 182 | public Flow(BaseNode startNode) { this.start(startNode); } 183 | 184 | public BaseNode start(BaseNode startNode) { 185 | this.startNode = Objects.requireNonNull(startNode, "Start node cannot be null"); 186 | return startNode; 187 | } 188 | 189 | @Override public final String exec(Void prepResult) { 190 | throw new UnsupportedOperationException("Flow.exec() is internal and should not be called directly."); 191 | } 192 | 193 | 194 | @SuppressWarnings({"unchecked", "rawtypes"}) // Raw types needed for successors map 195 | protected String orchestrate(Map sharedContext, Map initialParams) { 196 | if (startNode == null) { logWarn("Flow started with no start node."); return null; } 197 | BaseNode currentNode = this.startNode; 198 | String lastAction = null; 199 | Map currentParams = new HashMap<>(this.params); 200 | if (initialParams != null) { currentParams.putAll(initialParams); } 201 | while (currentNode != null) { 202 | currentNode.setParams(currentParams); 203 | lastAction = (String) ((BaseNode)currentNode).internalRun(sharedContext); 204 | currentNode = currentNode.getNextNode(lastAction); 205 | } 206 | return lastAction; 207 | } 208 | 209 | @Override 210 | protected String internalRun(Map sharedContext) { 211 | Void prepRes = prep(sharedContext); 212 | String orchRes = orchestrate(sharedContext, null); 213 | return post(sharedContext, prepRes, orchRes); 214 | } 215 | 216 | /** Post method for the Flow itself. Default returns the last action from orchestration. */ 217 | @Override public String post(Map sharedContext, Void prepResult, String execResult) { 218 | return execResult; 219 | } 220 | } 221 | 222 | /** 223 | * A flow that runs its entire sequence for each parameter set from `prepBatch`. 224 | */ 225 | public static abstract class BatchFlow extends Flow { 226 | public BatchFlow() { super(); } 227 | public BatchFlow(BaseNode startNode) { super(startNode); } 228 | 229 | public abstract List> prepBatch(Map sharedContext); 230 | /** Post method MUST return a String action, or null for the default action. */ 231 | public abstract String postBatch(Map sharedContext, List> batchPrepResult); 232 | 233 | @Override 234 | protected String internalRun(Map sharedContext) { 235 | List> batchParamsList = this.prepBatch(sharedContext); 236 | if (batchParamsList == null) { batchParamsList = Collections.emptyList(); } 237 | for (Map batchParams : batchParamsList) { 238 | Map currentRunParams = new HashMap<>(this.params); 239 | if (batchParams != null) { currentRunParams.putAll(batchParams); } 240 | orchestrate(sharedContext, currentRunParams); // Run for side-effects 241 | } 242 | return postBatch(sharedContext, batchParamsList); 243 | } 244 | 245 | @Override 246 | public final String post(Map sharedContext, Void prepResult, String execResult) { 247 | throw new UnsupportedOperationException("Use postBatch for BatchFlow, not post."); 248 | } 249 | } 250 | 251 | } --------------------------------------------------------------------------------