diff --git a/.gitignore b/.gitignore index 78ff8ff1..e2821da3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,5 @@ # Generated files # ################### -node_modules -node -target-grunt -.sass-cache -.grunt -C: .project .classpath .settings @@ -13,12 +7,7 @@ generated # Compiled source # ################### -# *.com *.class -*.dll -*.exe -*.o -*.so # Packages # ############ @@ -39,8 +28,6 @@ generated # Logs and databases # ###################### *.log -*.sql -*.sqlite # OS generated files # ###################### diff --git a/CHANGELOG.md b/CHANGELOG.md index 231727cb..dd776b83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to `knotx-fragments` will be documented in this file. ## Unreleased List of changes that are finished but not yet released in any final version. +- [PR-62](https://github.com/Knotx/knotx-fragments/pull/62) - - [PR-51](https://github.com/Knotx/knotx-fragments/pull/51) - Introduces extendable task definition allowing to define different node types and custom task providers. Marks the `actions` task configuration entry as deprecated, introduces `subtasks` instread. - [PR-56](https://github.com/Knotx/knotx-fragments/pull/56) - Makes composite node identifiers changeable. Renames `ActionNode` to `SingleNode`. - [PR-55](https://github.com/Knotx/knotx-fragments/pull/55) - Action log mechanism implementation. Renames `ActionFatalException` to `NodeFatalException`. diff --git a/handler/README.md b/handler/README.md index 0ffa0d61..46245ab4 100644 --- a/handler/README.md +++ b/handler/README.md @@ -7,8 +7,8 @@ a standard [HTTP Server routing handler](https://github.com/Knotx/knotx-server-h Fragments handler evaluates all fragments independently in a map-reduce fashion. It delegates fragment processing to the [Fragment Engine](https://github.com/Knotx/knotx-fragments/tree/master/handler/engine). The engine checks if the fragment requires processing and if it does then fragment processing starts. -modifications. Order and transitions between each of these executions is represented as a directed graph. -A single graph is called a `Task`. +Order and transitions between each of these executions is represented as a directed acyclic graph +(DAG). A single graph is called a `Task`. The diagram below depicts the map-reduce logic using [Marble Diagrams for Reactive Streams](https://medium.com/@jshvarts/read-marble-diagrams-like-a-pro-3d72934d3ef5): @@ -24,104 +24,131 @@ such a case the `collect` diagram represents fragments joining. Read more about the benefits [here](http://knotx.io/blog/configurable-integrations/). -## Task -Task decompose business logic into lightweight independent parts. Those parts are graph nodes, -connected by transitions. Graph node can, for example, represent some REST API invocation. So a task -is a directed graph of nodes. -``` -(A) ───> (B) ───> (C) - └──> (D) -``` +## How to configure +For all configuration fields and their defaults consult [FragmentsHandlerOptions](https://github.com/Knotx/knotx-fragments/blob/master/handler/core/docs/asciidoc/dataobjects.adoc#fragmentshandleroptions). + +In general: +- it gets fragments from a request, collects only those that require processing checking fragment's +configuration `taskNameKey` key +- it finds [task](https://github.com/Knotx/knotx-fragments/tree/master/handler/engine#task) factory +from `taskFactories` that can create a task +- task factory constructs a task (DAG - directed acyclic graph) +- [Fragment Engine](https://github.com/Knotx/knotx-fragments/tree/master/handler/engine) continues +further fragment's processing + +Detailed description of each configuration option is described in the next subsection. + +### TaskFactoryOptions +The `taskFactories` is an array of [TaskFactoryOptions](https://github.com/Knotx/knotx-fragments/blob/feature/%2349-node-factories-with-common-configs/handler/core/docs/asciidoc/dataobjects.adoc#taskfactoryoptions) +that contains configs for registered [task factories](https://github.com/Knotx/knotx-fragments/blob/master/handler/core/src/main/java/io/knotx/fragments/task/TaskFactory.java) +implementations. + +All task factory implementations are registered using a simple service-provider loading facility - +[Service Loader](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html). + +# Default task factory +It is the default task factory containing a list of supported tasks' names with their definition. A +definition represents a directed acyclic graph (DAG). +It introduces its custom graph nodes +- [action](#action-node) +- [subtasks](#subtasks-node) + +## How does it work +Task factory creates a task based on its configuration. It registers graph node factories, delegates +node initialization to them and joins all nodes with transitions. + +The task is an [identifiable graph](https://github.com/Knotx/knotx-fragments/blob/master/handler/engine/src/main/java/io/knotx/fragments/engine/Task.java) +that describes the way fragment should be processed. It is a part of Fragment Engine's API. -Tasks are configured with HOCON configuration in form of a dictionary (`taskName -> definition`): +## How to configure +For all configuration fields and their defaults consult [DefaultTaskFactoryConfig](https://github.com/Knotx/knotx-fragments/blob/master/handler/core/docs/asciidoc/dataobjects.adoc#defaulttaskfactoryconfig). + +In general: +- it defines supported tasks (by name) containing graph configuration in `tasks` +- a graph configuration is converted to task with node factories defined in `nodeFactories` + +### Tasks +[Tasks](https://github.com/Knotx/knotx-fragments/tree/master/handler/engine#task) are configured in +the form of a dictionary (`taskName -> graph logic`): ```hocon tasks { # unique task name myTask { - # task provider options and graph logic + # graph configuration } } ``` -Let's see how tasks are instantiated. A task specifies its provider factory (with options) and its -graph logic: +A graph configuration starts with a root [node](#nodes) definition. Each node sets some logic to perform over a fragment + and outgoing edges (called [transitions](#transition)). So there are two sections: ```hocon -factory = factory-name -config { - # factory options +node { + # node options (fragment processing logic) } -graph { - # graph logic +onTransitions { + # node outgoing edges } ``` -In most cases the default task provider (`configuration`) is used, so the definition can be -simplified to: -```hocon -tasks { - # unique task name - myTask { - # graph logic - } -} -``` -> Note: -> Custom tasks providers can be easily added with custom [factories](https://github.com/Knotx/knotx-fragments/blob/master/handler/core/src/main/java/io/knotx/fragments/task/TaskProviderFactory.java) -> that register in [Task Factory](https://github.com/Knotx/knotx-fragments/blob/master/handler/core/src/main/java/io/knotx/fragments/task/TaskFactory.java). - -#### Graph -As already mentioned, the task logic is defined in the form of a directed graph. Moreover, this graph -is acyclic and each of its nodes can be reached only from exactly one path (transition). These -properties enable us to treat the Task as a tree structure. So we need to define the root node of -the tree first. -```hocon -graph { - # rootNodeDefinition -} -``` -Each graph node sets fragment logic to perform and outgoing edges (called Transitions). So the -`rootNodeDefinition` configuration looks like: +#### Node +Node is described [here](https://github.com/Knotx/knotx-fragments/tree/master/handler/engine#node). +Each task provider can provide its custom nodes. + +The default task provider allows registering node factories that are used during the graph +configuration. So each node defines its custom factory and options: ```hocon -graph { - node { # required - # node options (fragment processing logic) - } - onTransitions { - # node outgoing edges +node { + factory = factory-name + config { + # node options } } ``` -There are two sections: -- `node` defines a fragment processing logic -- `onTransitions` is a map that represents outgoing edges in a graph -##### Node -Node is described [here](https://github.com/Knotx/knotx-fragments/tree/master/handler/engine#node). +The `factory` parameter specifies a node factory name, `config` contains all configs passed to +the factory. -Fragments Handler introduces custom node types that are finally converted to the -[engine node types](https://github.com/Knotx/knotx-fragments/tree/master/handler/engine#node-types). -It allows to quickly add new node types, with different configuration options, without modifying -the engine. +All custom node types that are finally converted to the +[generic node types](https://github.com/Knotx/knotx-fragments/tree/master/handler/engine#node-types). -Each node defines its custom factory. The configuration is simple: +#### Transition +A directed graph consists of nodes and edges. Edges are called +[transitions](https://github.com/Knotx/knotx-fragments/tree/master/handler/engine#transition). + +Their configuration looks like: ```hocon -node { - factory = factory-name - config { - # factory config +onTransitions { + _success { + # next node when _success transition + } + _error { + # next node when error occurs + } + customTransition { + # next node when custom transition } } ``` -The `factory` parameter specifies a node factory name, `config` contains all options passed to -the factory. -Fragments Handler provides two node implementations: -- **Action node** that represents simple steps in a graph such as integration with a data source -- **Subtasks node** that is a list of unnamed tasks (subtasks) that are evaluated in parallel +### Node factories +The `nodeFactories` is an array of [NodeFactoryOptions](https://github.com/Knotx/knotx-fragments/blob/master/handler/core/docs/asciidoc/dataobjects.adoc#nodefactoryoptions) +that contains configs for registered [node factories](https://github.com/Knotx/knotx-fragments/blob/master/handler/core/src/main/java/io/knotx/fragments/task/factory/node/NodeFactory.java) +implementations. + +All node factory implementations are registered using a simple service-provider loading facility - +[Service Loader](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html). + +#### Action node factory +It is implemented by the [ActionNodeFactory](https://github.com/Knotx/knotx-fragments/blob/master/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionNodeFactory.java) +class. Its name is `action`. It is configured with [ActionNodeFactoryConfig](https://github.com/Knotx/knotx-fragments/blob/master/handler/core/docs/asciidoc/dataobjects.adoc#actionnodefactoryconfig). -###### Action node +In general: +- it declares [actions](#actions) that are used in action node declarations +- global node log level that is passed to actions -An *action node* declares an [action](#actions) to execute by its name: +##### Action node declaration +An action node factory creates action nodes. An *action node* declares an [action](#actions) to +execute by its name: ```hocon node { factory = action @@ -134,14 +161,13 @@ node { The above example specifies the action node that delegates processing to the `reference-to-action` action and has no transitions. -Knot.x allows simplifying action nodes declaration: +The default task factory allows simplifying action nodes declaration with the following syntax sugar: ```hocon action = reference-to-action # onTransitions { } ``` -A nice syntax sugar! -####### Logs +##### Logs Action node appends a single [fragment's log](https://github.com/Knotx/knotx-fragments/tree/master/handler/engine#fragments-log) entry: @@ -167,7 +193,12 @@ So it supports both [actions](#actions) and [behaviours](#behaviours). Actions d [logger](https://github.com/Knotx/knotx-fragments/blob/feature/%2347-action-log-structure/handler/api/src/main/java/io/knotx/fragments/handler/api/actionlog/ActionLogBuilder.java) implementation that hides syntax complexity. -###### Subtasks node +#### Subtasks node factory +A subtask is nothing else than a subgraph defined inside the task. +Creating subtasks is implemented in the [SubtasksNodeFactory](https://github.com/Knotx/knotx-fragments/blob/master/handler/core/src/main/java/io/knotx/fragments/task/factory/node/subtasks/SubtasksNodeFactory.java) +class. Its name is `subtasks`. Its configuration is empty. + +##### Subtasks node declaration Subtasks node is a node containing a list of subtasks. It evaluates all of them sequentially. However, all the operations are non-blocking, so it doesn't wait for previous subtasks to finish. Because of that, they are effectively executed in parallel @@ -222,8 +253,8 @@ two independent tasks (graphs) with one node (action). See the [example section](#the-example) for a more complex scenario. Before we see the full power of graphs, we need to understand how nodes are connected. -####### Logs -Subtasks node appends a single [fragment's log](https://github.com/Knotx/knotx-fragments/tree/feature/%2347-action-log-structure/handler/engine#fragments-log) +##### Logs +Subtasks node appends a single [fragment's log](https://github.com/Knotx/knotx-fragments/tree/master/handler/engine#fragments-log) entry when all subgraphs are processed: | Task | Node identifier | Node status | Transition | Node Log | @@ -232,36 +263,7 @@ entry when all subgraphs are processed: Please note that the node log is empty. -##### Transitions -A directed graph consists of nodes and edges. Edges are called transitions. Their configuration -looks like: -```hocon -onTransitions { - _success { - # next node when _success transition - } - _error { - # next node when error occurs - } - customTransition { - # next node when custom transition - } -} -``` -Transition is a simple text. The `_success` and `_error` transitions are the default ones. However, -they are not mandatory! - -There are two important rules to remember: -> If a node responds with *_success* transition, but the transition is not configured, then ->processing is finished. - -> If a node responds with *_error* transition, but the transition is not configured, then an ->exception is returned. - -Nodes can declare custom transitions. Custom transitions allow to react to non standard situations -such as data sources timeouts, fallbacks etc. - -### The example +### Example The example below collects data about the book and its authors from external APIs. Book and authors APIs accept ISBN and respond with JSON. We can invoke those APIs in parallel. However, the book API does not contain the score. There is a separate service that accepts secret @@ -272,6 +274,15 @@ The above logic can be easily transformed into the task: ```hocon tasks { book-and-author-task { + config { + actions { + book-rest-api {} + author-rest-api {} + book-from-cache{} + score-api {} + score-estimation {} + } + } subtasks = [ { # 1st subtask action = book-rest-api # HTTP Action @@ -333,29 +344,36 @@ timeouts and errors from APIs. Please note that no error strategy has been defined for authors API yet. However, it can be easily configured in the future when business agrees on the fallback logic. +# Actions +Action is a simple function that converts a fragment to the new one and responds with the +[transition](#transition). Its contract is the same as the +[node](https://github.com/Knotx/knotx-fragments/tree/master/handler/engine#node)'s one. So an +action defines a fragment's processing logic. Actions implement the +[Action](https://github.com/Knotx/knotx-fragments/blob/master/handler/api/src/main/java/io/knotx/fragments/handler/api/Action.java) +interface. -## Actions -Action defines action node logic. Actions can integrate with external data sources, do some fragments -modifications or fetch data. A data source response is saved in a Fragment's payload (JSON object) -under an Action's name key and a "\_result" sub-key: -```json -{ - "book": { - "_result": { } - } -} -``` +Action can integrate with external data sources, do some fragments modifications or even +update some database records. +They can update a fragment, change all fragment's payload values. However, they should +store their data in the payload (JSON object) under the action's name key. + +Actions can have some [behaviours](#behaviours). Those behaviours come with some functionality, +such as caching, however, they are not specific for particular action implementation. -Actions are divided in two types: -- `simple actions` that actually modify the fragment (e.g. integrate with data sources and saves the payload) -- `behaviours` that wrap simple actions and add some "behaviour" +## Action factory +All actions provide [factories](https://github.com/Knotx/knotx-fragments/blob/master/handler/api/src/main/java/io/knotx/fragments/handler/api/ActionFactory.java) +that are registered using a simple service-provider loading facility - +[Service Loader](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html). -### Simple Actions +For all configuration fields and their defaults consult [ActionFactoryOptions](https://github.com/Knotx/knotx-fragments/blob/master/handler/core/docs/asciidoc/dataobjects.adoc#actionfactoryoptions). -#### HTTP Action -The HTTP Action fetches JSON data from REST APIs (GET request). See more [here](https://github.com/Knotx/knotx-data-bridge/tree/master/http). +## Action's types -#### Inline Body Action +### HTTP Action +The HTTP Action fetches JSON data from REST APIs (GET request). See more +[here](https://github.com/Knotx/knotx-data-bridge/tree/master/http). + +### Inline Body Action Inline Body Action replaces Fragment body with specified one. Its configuration looks like: ```hocon factory = "inline-body" @@ -366,7 +384,7 @@ config { ``` The default `body` value is empty content. -#### Inline Payload Action +### Inline Payload Action Inline Payload Action puts JSON / JSON Array in Fragment payload with a specified key (alias). Its configuration looks like: ```hocon @@ -384,7 +402,7 @@ config { ``` The default `alias` is action alias. -#### Payload To Body Action +### Payload To Body Action Payload To Body Action copies to Fragment body specified payload key value. Its configuration looks like: ```hocon factory = payload-to-body @@ -410,13 +428,12 @@ and key value `someKey.someNestedKey` body value will look like: } ``` -### Behaviours - -Behaviours wrap other behaviours or simple actions and delegate a fragment to them (for processing). +## Behaviours +Behaviours wrap other behaviours or [actions](#actions) and delegate a fragment processing to them. They can introduce some stability patterns such as retires, it means that they can call a wrapped -Action many times. +action many times. -#### Circuit Breaker Behaviour +### Circuit Breaker Behaviour It wraps a simple action with the [Circuit Breaker implementation from Vert.x](https://vertx.io/docs/vertx-circuit-breaker/java/). Its configuration looks like: ```hocon @@ -437,7 +454,7 @@ doAction = product The `doAction` attribute specifies a wrapped simple action by its name. When `doAction` throws an error or times out then the custom `fallback` transition is returned. -#### In-memory Cache Behaviour +### In-memory Cache Behaviour It wraps a simple action with cache. It caches a payload values added by a `doAction` action and puts cached values in next invocations. It uses in-memory Guava cache implementation. The configuration looks like: @@ -456,4 +473,4 @@ doAction = product-cb ``` Please note that cacheKey can be parametrized with request data like params, headers etc. Read [Knot.x HTTP Server Common Placeholders](https://github.com/Knotx/knotx-server-http/tree/master/common/placeholders) -documentation for more details. +documentation for more details. \ No newline at end of file diff --git a/handler/api/src/main/java/io/knotx/fragments/handler/api/actionlog/ActionLogLevel.java b/handler/api/src/main/java/io/knotx/fragments/handler/api/actionlog/ActionLogLevel.java index a736707a..73058668 100644 --- a/handler/api/src/main/java/io/knotx/fragments/handler/api/actionlog/ActionLogLevel.java +++ b/handler/api/src/main/java/io/knotx/fragments/handler/api/actionlog/ActionLogLevel.java @@ -22,7 +22,7 @@ public enum ActionLogLevel { INFO("info"), ERROR("error"); - public static final String CONFIG_KEY_NAME = "logLevel"; + private final String level; ActionLogLevel(String level) { diff --git a/handler/api/src/main/java/io/knotx/fragments/handler/api/actionlog/ActionLogger.java b/handler/api/src/main/java/io/knotx/fragments/handler/api/actionlog/ActionLogger.java index f16575bc..91ecfff1 100644 --- a/handler/api/src/main/java/io/knotx/fragments/handler/api/actionlog/ActionLogger.java +++ b/handler/api/src/main/java/io/knotx/fragments/handler/api/actionlog/ActionLogger.java @@ -15,15 +15,13 @@ */ package io.knotx.fragments.handler.api.actionlog; -import static io.knotx.fragments.handler.api.actionlog.ActionLogLevel.CONFIG_KEY_NAME; import static io.knotx.fragments.handler.api.actionlog.ActionLogLevel.INFO; +import io.vertx.core.json.JsonObject; import java.time.Instant; import java.util.Objects; import java.util.function.Function; -import io.vertx.core.json.JsonObject; - public class ActionLogger { private final ActionLogLevel actionLogLevel; @@ -34,10 +32,6 @@ private ActionLogger(String alias, ActionLogLevel actionLogLevel) { this.builder = new ActionLogBuilder(alias); } - public static ActionLogger create(String alias, JsonObject config) { - return ActionLogger.create(alias, config.getString(CONFIG_KEY_NAME)); - } - public static ActionLogger create(String alias, ActionLogLevel actionLogLevel) { return new ActionLogger(alias, actionLogLevel); } diff --git a/handler/core/docs/asciidoc/dataobjects.adoc b/handler/core/docs/asciidoc/dataobjects.adoc index 8657c4fe..305f45e1 100644 --- a/handler/core/docs/asciidoc/dataobjects.adoc +++ b/handler/core/docs/asciidoc/dataobjects.adoc @@ -1,15 +1,34 @@ = Cheatsheets -[[ActionNodeConfigOptions]] -== ActionNodeConfigOptions +[[ActionFactoryOptions]] +== ActionFactoryOptions + + +[cols=">25%,25%,50%"] +[frame="topbot"] +|=== +^|Name | Type ^| Description +|[[config]]`@config`|`Json object`|+++ +Sets Action configuration that is passed to Action. ++++ +|[[doAction]]`@doAction`|`String`|+++ +Sets the name of the base Action that will be triggered while creating current Action. In not set (null), given action will have no base actions. ++++ +|[[factory]]`@factory`|`String`|+++ +Sets Action factory name. ++++ +|=== + +[[ActionNodeConfig]] +== ActionNodeConfig ++++ - Action node configuration model. It is model for link JSON object. + Action node configuration model. It is model for JSON object.
  node {
    factory = action
-   config { //represented by ActionNodeConfigOptions
+   config { //represented by ActionNodeConfig
      ...
    }
  }
@@ -27,40 +46,65 @@ Sets link name. The specified Action is executed
 +++
 |===
 
-[[ActionOptions]]
-== ActionOptions
+[[ActionNodeFactoryConfig]]
+== ActionNodeFactoryConfig
 
+++++
+ Action Node factory config model.
+++++
+'''
 
 [cols=">25%,25%,50%"]
 [frame="topbot"]
 |===
 ^|Name | Type ^| Description
-|[[config]]`@config`|`Json object`|+++
-Sets Action configuration that is passed to Action.
+|[[actions]]`@actions`|`link:dataobjects.html#ActionFactoryOptions[ActionFactoryOptions]`|+++
+The dictionary maps action name to action factory options.
 +++
-|[[doAction]]`@doAction`|`String`|+++
-Sets the name of the base Action that will be triggered while creating current Action. In not set (null), given action will have no base actions.
+|===
+
+[[DefaultTaskFactoryConfig]]
+== DefaultTaskFactoryConfig
+
+++++
+ Default Task Factory config model.
+++++
+'''
+
+[cols=">25%,25%,50%"]
+[frame="topbot"]
+|===
+^|Name | Type ^| Description
+|[[logLevel]]`@logLevel`|`String`|+++
+The global node log level.
 +++
-|[[factory]]`@factory`|`String`|+++
-Sets Action factory name.
+|[[nodeFactories]]`@nodeFactories`|`Array of link:dataobjects.html#NodeFactoryOptions[NodeFactoryOptions]`|+++
+The array/list of graph node factory options defines node factories taking part in the creation
+ of graph.
++++
+|[[taskNameKey]]`@taskNameKey`|`String`|+++
+The fragment's configuration key specifies a task assigned to a fragment by the task name.
++++
+|[[tasks]]`@tasks`|`link:dataobjects.html#GraphNodeOptions[GraphNodeOptions]`|+++
+The dictionary that maps a task name to a directed acyclic graph (DAG) of nodes.
 +++
 |===
 
 [[FragmentsHandlerOptions]]
 == FragmentsHandlerOptions
 
+++++
+ Fragments Handler options model.
+++++
+'''
 
 [cols=">25%,25%,50%"]
 [frame="topbot"]
 |===
 ^|Name | Type ^| Description
-|[[actions]]`@actions`|`link:dataobjects.html#ActionOptions[ActionOptions]`|+++
-Sets named actions with their factory configuration.
-+++
-|[[logLevel]]`@logLevel`|`String`|-
-|[[taskKey]]`@taskKey`|`String`|-
-|[[tasks]]`@tasks`|`link:dataobjects.html#TaskOptions[TaskOptions]`|+++
-Sets Task list, which are named, directed graphs of Actions.
+|[[taskFactories]]`@taskFactories`|`Array of link:dataobjects.html#TaskFactoryOptions[TaskFactoryOptions]`|+++
+The array/list of task factory options defines factories taking part in the creation of tasks. First
+ items on the list have the highest priority.
 +++
 |===
 
@@ -68,33 +112,7 @@ Sets Task list, which are named, directed graphs of ActionsTransitions.
-
- It represents JSON configuration:
- 
- {
-   node = {
-     factory = action
-     config {
-       action = a
-     }
-   }
-   onTransitions {
-     _success {
-       node = {
-         factory = action
-         config {
-           action = b
-         }
-       }
-     }
-   }
- }
- 
- - Please note that Transitions define next graph nodes. + Graph node options model. ++++ ''' @@ -103,20 +121,51 @@ Sets Task list, which are named, directed graphs of ActionsActionNodeFactory.NAME and configures the action. +++ |[[actions]]`@actions`|`Array of link:dataobjects.html#GraphNodeOptions[GraphNodeOptions]`|+++ - +Sets a node factory name to SubtasksNodeFactory.NAME and configures subgraphs. +++ -|[[composite]]`@composite`|`Boolean`|- |[[node]]`@node`|`link:dataobjects.html#NodeOptions[NodeOptions]`|+++ -Sets node options defining node factory and its configuration. +Node options define a node factory and its configuration. +++ |[[onTransitions]]`@onTransitions`|`link:dataobjects.html#GraphNodeOptions[GraphNodeOptions]`|+++ -Sets outgoing graph node edges, called Transitions. Transition is String, onTransitions map links Transition with next graph node. +The outgoing graph node edges, called transitions. A transition is named graph edge that + defines the next graph node in fragment's processing. +++ |[[subtasks]]`@subtasks`|`Array of link:dataobjects.html#GraphNodeOptions[GraphNodeOptions]`|+++ +Sets a node factory name to SubtasksNodeFactory.NAME and configures subgraphs. ++++ +|=== + +[[LogLevelConfig]] +== LogLevelConfig + + +[cols=">25%,25%,50%"] +[frame="topbot"] +|=== +^|Name | Type ^| Description +|[[logLevel]]`@logLevel`|`String`|- +|=== + +[[NodeFactoryOptions]] +== NodeFactoryOptions + +++++ + Node Factory options model. +++++ +''' +[cols=">25%,25%,50%"] +[frame="topbot"] +|=== +^|Name | Type ^| Description +|[[config]]`@config`|`Json object`|+++ +The JSON object that contains node factory configuration entries. ++++ +|[[factory]]`@factory`|`String`|+++ +The node factory name that identifies NodeFactory implementation. +++ |=== @@ -140,20 +189,11 @@ Sets node factory name +++ |=== -[[SubtasksNodeConfigOptions]] -== SubtasksNodeConfigOptions +[[SubtasksNodeConfig]] +== SubtasksNodeConfig ++++ - Subtask node configuration. It is model for link JSON object. - -
- node {
-   factory = subtasks
-   config { //represented by SubtasksNodeConfigOptions
-     ...
-   }
- }
- 
+ Subtask Node configuration. ++++ ''' @@ -162,16 +202,16 @@ Sets node factory name |=== ^|Name | Type ^| Description |[[subtasks]]`@subtasks`|`Array of link:dataobjects.html#GraphNodeOptions[GraphNodeOptions]`|+++ -Sets list of link that represents link - that will be executed in parallel. +The array/list of subgraphs/subtasks that can be executed in parallel. +++ |=== -[[TaskOptions]] -== TaskOptions +[[TaskFactoryOptions]] +== TaskFactoryOptions ++++ - Task options. + Task Factory options model. It specifies task factory by its name and provides task factory + config. ++++ ''' @@ -180,13 +220,10 @@ Sets list of link that represents link |=== ^|Name | Type ^| Description |[[config]]`@config`|`Json object`|+++ -Gets task provider factory configuration. +The JSON object that contains task factory configuration entries. +++ |[[factory]]`@factory`|`String`|+++ -Sets task provider factory name -+++ -|[[graph]]`@graph`|`link:dataobjects.html#GraphNodeOptions[GraphNodeOptions]`|+++ -Sets task graph. +The task factory name that identifies TaskFactory implementation. +++ |=== diff --git a/handler/core/handler/common/customTask.conf b/handler/core/handler/common/customTask.conf new file mode 100644 index 00000000..82859214 --- /dev/null +++ b/handler/core/handler/common/customTask.conf @@ -0,0 +1,27 @@ +tasks { + success-task { + graph { + action = success-action + } + } +} + +nodeFactories = [ + { + factory = action + config { + actions { + success-action { + factory = test-action + config { + transition = _success + body = "custom" + } + } + } + } + } + { + factory = subtasks + } +] \ No newline at end of file diff --git a/handler/core/handler/common/failingTask.conf b/handler/core/handler/common/failingTask.conf new file mode 100644 index 00000000..300c5c22 --- /dev/null +++ b/handler/core/handler/common/failingTask.conf @@ -0,0 +1,26 @@ +tasks { + failing-task { + graph { + action = failing-action + } + } +} + +nodeFactories = [ + { + factory = action + config { + actions { + failing-action { + factory = test-action + config { + transition = not-existing-transition + } + } + } + } + } + { + factory = subtasks + } +] \ No newline at end of file diff --git a/handler/core/handler/common/successTask.conf b/handler/core/handler/common/successTask.conf new file mode 100644 index 00000000..2c933200 --- /dev/null +++ b/handler/core/handler/common/successTask.conf @@ -0,0 +1,27 @@ +tasks { + success-task { + graph { + action = success-action + } + } +} + +nodeFactories = [ + { + factory = action + config { + actions { + success-action { + factory = test-action + config { + transition = _success + body = "success" + } + } + } + } + } + { + factory = subtasks + } +] \ No newline at end of file diff --git a/handler/core/handler/manyTaskFactoriesWithTheSameName.conf b/handler/core/handler/manyTaskFactoriesWithTheSameName.conf new file mode 100644 index 00000000..a104a973 --- /dev/null +++ b/handler/core/handler/manyTaskFactoriesWithTheSameName.conf @@ -0,0 +1,13 @@ +taskFactories = [ + { + factory = default + config {include required(classpath("handler/common/successTask.conf"))} + } + { + factory = default + config {include required(classpath("handler/common/customTask.conf"))} + config { + taskNameKey = task + } + } +] \ No newline at end of file diff --git a/handler/core/handler/singleTaskFactoryWithFailingTask.conf b/handler/core/handler/singleTaskFactoryWithFailingTask.conf new file mode 100644 index 00000000..7ec10ca1 --- /dev/null +++ b/handler/core/handler/singleTaskFactoryWithFailingTask.conf @@ -0,0 +1,6 @@ +taskFactories = [ + { + factory = default + config { include required(classpath("handler/common/failingTask.conf")) } + } +] \ No newline at end of file diff --git a/handler/core/handler/singleTaskFactoryWithSuccessTask.conf b/handler/core/handler/singleTaskFactoryWithSuccessTask.conf new file mode 100644 index 00000000..ea3e4bb7 --- /dev/null +++ b/handler/core/handler/singleTaskFactoryWithSuccessTask.conf @@ -0,0 +1,6 @@ +taskFactories = [ + { + factory = default + config { include required(classpath("handler/common/successTask.conf")) } + } +] \ No newline at end of file diff --git a/handler/core/handler/taskFactoryNameNotDefined.conf b/handler/core/handler/taskFactoryNameNotDefined.conf new file mode 100644 index 00000000..4e222c46 --- /dev/null +++ b/handler/core/handler/taskFactoryNameNotDefined.conf @@ -0,0 +1,4 @@ +taskFactories = [ + { + } +] \ No newline at end of file diff --git a/handler/core/handler/taskFactoryNotFound.conf b/handler/core/handler/taskFactoryNotFound.conf new file mode 100644 index 00000000..79cdce2a --- /dev/null +++ b/handler/core/handler/taskFactoryNotFound.conf @@ -0,0 +1,5 @@ +taskFactories = [ + { + factory = invalid + } +] \ No newline at end of file diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/exception/ConfigurationException.java b/handler/core/src/main/java/io/knotx/fragments/ConfigurationException.java similarity index 94% rename from handler/core/src/main/java/io/knotx/fragments/handler/exception/ConfigurationException.java rename to handler/core/src/main/java/io/knotx/fragments/ConfigurationException.java index 424fa001..001971e4 100644 --- a/handler/core/src/main/java/io/knotx/fragments/handler/exception/ConfigurationException.java +++ b/handler/core/src/main/java/io/knotx/fragments/ConfigurationException.java @@ -15,7 +15,7 @@ * * The code comes from https://github.com/tomaszmichalak/vertx-rx-map-reduce. */ -package io.knotx.fragments.handler.exception; +package io.knotx.fragments; public abstract class ConfigurationException extends RuntimeException { diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/FragmentsHandler.java b/handler/core/src/main/java/io/knotx/fragments/handler/FragmentsHandler.java index 37f73838..a891d229 100644 --- a/handler/core/src/main/java/io/knotx/fragments/handler/FragmentsHandler.java +++ b/handler/core/src/main/java/io/knotx/fragments/handler/FragmentsHandler.java @@ -17,8 +17,6 @@ */ package io.knotx.fragments.handler; -import static com.google.common.base.Predicates.alwaysTrue; - import io.knotx.fragments.api.Fragment; import io.knotx.fragments.engine.FragmentEvent; import io.knotx.fragments.engine.FragmentEvent.Status; @@ -26,10 +24,6 @@ import io.knotx.fragments.engine.FragmentEventContextTaskAware; import io.knotx.fragments.engine.FragmentsEngine; import io.knotx.fragments.engine.Task; -import io.knotx.fragments.handler.action.ActionProvider; -import io.knotx.fragments.handler.api.ActionFactory; -import io.knotx.fragments.handler.options.FragmentsHandlerOptions; -import io.knotx.fragments.task.TaskFactory; import io.knotx.server.api.context.ClientRequest; import io.knotx.server.api.context.RequestContext; import io.knotx.server.api.context.RequestEvent; @@ -39,40 +33,38 @@ import io.reactivex.Single; import io.vertx.core.Handler; import io.vertx.core.json.JsonObject; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; import io.vertx.reactivex.core.Vertx; import io.vertx.reactivex.ext.web.RoutingContext; -import java.util.Iterator; import java.util.List; -import java.util.ServiceLoader; import java.util.function.Predicate; -import java.util.function.Supplier; import java.util.stream.Collectors; public class FragmentsHandler implements Handler { - private final FragmentsEngine engine; + private static final Logger LOGGER = LoggerFactory.getLogger(FragmentsHandler.class); + private final RequestContextEngine requestContextEngine; - private final TaskFactory taskFactory; - FragmentsHandler(Vertx vertx, JsonObject config) { - FragmentsHandlerOptions options = new FragmentsHandlerOptions(config); - ActionProvider proxyProvider = new ActionProvider(options.getActions(), - supplyFactories(), options.getLogLevel(), vertx.getDelegate()); - taskFactory = new TaskFactory(options.getTaskKey(), options.getTasks(), proxyProvider); + private final FragmentsEngine engine; + private final TaskProvider taskProvider; + + FragmentsHandler(Vertx vertx, JsonObject options) { + FragmentsHandlerOptions handlerOptions = new FragmentsHandlerOptions(options); + taskProvider = new TaskProvider(handlerOptions.getTaskFactories(), vertx); engine = new FragmentsEngine(vertx); requestContextEngine = new DefaultRequestContextEngine(getClass().getSimpleName()); } @Override public void handle(RoutingContext routingContext) { - RequestContext requestContext = routingContext.get(RequestContext.KEY); + final RequestContext requestContext = routingContext.get(RequestContext.KEY); final List fragments = routingContext.get("fragments"); + final ClientRequest clientRequest = requestContext.getRequestEvent().getClientRequest(); - ClientRequest clientRequest = requestContext.getRequestEvent().getClientRequest(); - - Single.just(fragments) - .map(f -> toEvents(f, clientRequest)) - .flatMap(engine::execute) + Single> doHandle = doHandle(fragments, clientRequest); + doHandle .doOnSuccess(events -> putFragments(routingContext, events)) .map(events -> toHandlerResult(events, requestContext)) .subscribe( @@ -82,22 +74,22 @@ public void handle(RoutingContext routingContext) { ); } - private RoutingContext putFragments(RoutingContext routingContext, List events) { - return routingContext.put("fragments", retrieveFragments(events, alwaysTrue())); + protected Single> doHandle(List fragments, + ClientRequest clientRequest) { + return Single.just(fragments) + .map(f -> toEvents(f, clientRequest)) + .flatMap(engine::execute); } - private Supplier> supplyFactories() { - return () -> { - ServiceLoader factories = ServiceLoader - .load(ActionFactory.class); - return factories.iterator(); - }; + private void putFragments(RoutingContext routingContext, List events) { + routingContext.put("fragments", retrieveFragments(events, fragmentEvent -> true)); } private RequestEventHandlerResult toHandlerResult(List events, RequestContext requestContext) { - List failedFragments = retrieveFragments(events, hasStatus(Status.FAILURE)); + List failedFragments = retrieveFragments(events, + e -> e.getStatus() == Status.FAILURE); if (!failedFragments.isEmpty()) { return RequestEventHandlerResult.fail(buildErrorMessage(failedFragments)); @@ -120,10 +112,6 @@ private RequestEvent copyRequestEvent(RequestEvent requestEvent) { return new RequestEvent(requestEvent.getClientRequest(), requestEvent.getPayload()); } - private Predicate hasStatus(Status status) { - return e -> e.getStatus() == status; - } - private List retrieveFragments(List events, Predicate predicate) { return events.stream() @@ -134,12 +122,19 @@ private List retrieveFragments(List events, private List toEvents(List fragments, ClientRequest clientRequest) { + LOGGER.trace("Processing fragments [{}]", fragments); return fragments.stream() .map( fragment -> { FragmentEventContext fragmentEventContext = new FragmentEventContext( new FragmentEvent(fragment), clientRequest); - return taskFactory.newInstance(fragmentEventContext) + + return taskProvider.newInstance(fragmentEventContext) + .map(task -> { + LOGGER.trace("Created task [{}] for fragment [{}]", task, + fragmentEventContext.getFragmentEvent().getFragment().getId()); + return task; + }) .map( task -> new FragmentEventContextTaskAware(task, fragmentEventContext)) .orElseGet(() -> new FragmentEventContextTaskAware(new Task("_NOT_DEFINED"), diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/FragmentsHandlerOptions.java b/handler/core/src/main/java/io/knotx/fragments/handler/FragmentsHandlerOptions.java new file mode 100644 index 00000000..dd52f906 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/handler/FragmentsHandlerOptions.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.handler; + +import io.knotx.fragments.task.TaskFactoryOptions; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.core.json.JsonObject; +import java.util.List; +import java.util.Objects; + +/** + * Fragments Handler options model. + */ +@DataObject(generateConverter = true) +public class FragmentsHandlerOptions { + + private List taskFactories; + + public FragmentsHandlerOptions(JsonObject json) { + FragmentsHandlerOptionsConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject jsonObject = new JsonObject(); + FragmentsHandlerOptionsConverter.toJson(this, jsonObject); + return jsonObject; + } + + public List getTaskFactories() { + return taskFactories; + } + + /** + * The array/list of task factory options defines factories taking part in the creation of tasks. First + * items on the list have the highest priority. + * + * @param taskFactories - a list of task factory options + */ + public void setTaskFactories(List taskFactories) { + this.taskFactories = taskFactories; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FragmentsHandlerOptions that = (FragmentsHandlerOptions) o; + return Objects.equals(taskFactories, that.taskFactories); + } + + @Override + public int hashCode() { + return Objects.hash(taskFactories); + } + + @Override + public String toString() { + return "FragmentsHandlerOptions{" + + "taskFactories=" + taskFactories + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/TaskProvider.java b/handler/core/src/main/java/io/knotx/fragments/handler/TaskProvider.java new file mode 100644 index 00000000..0e608c9b --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/handler/TaskProvider.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * The code comes from https://github.com/tomaszmichalak/vertx-rx-map-reduce. + */ +package io.knotx.fragments.handler; + +import io.knotx.fragments.engine.FragmentEventContext; +import io.knotx.fragments.engine.Task; +import io.knotx.fragments.handler.exception.TaskFactoryNotFoundException; +import io.knotx.fragments.task.TaskFactory; +import io.knotx.fragments.task.TaskFactoryOptions; +import io.vertx.core.json.JsonObject; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import io.vertx.reactivex.core.Vertx; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; + +class TaskProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(TaskProvider.class); + + private List factories; + private final Vertx vertx; + + TaskProvider(List factoryOptions, Vertx vertx) { + this.vertx = vertx; + factories = initFactories(factoryOptions); + } + + Optional newInstance(FragmentEventContext eventContext) { + return factories.stream() + .filter(f -> f.accept(eventContext)) + .findFirst() + .map(f -> { + LOGGER.debug("Task factory [{}] accepts fragment [{}]", f.getName(), + eventContext.getFragmentEvent().getFragment().getId()); + return f; + }) + .map(f -> f.newInstance(eventContext)); + } + + private List initFactories(List optionsList) { + Map loadedFactories = loadFactories(); + + List result = new ArrayList<>(); + optionsList.forEach(options -> result.add( + configureFactory(loadedFactories, options.getFactory(), options.getConfig()))); + return result; + } + + private TaskFactory configureFactory(Map loadedFactories, String factory, + JsonObject config) { + LOGGER.info("Initializing task factory [{}] with config [{}]", factory, config); + return Optional.ofNullable(loadedFactories.get(factory)) + .map(f -> f.configure(config, vertx)) + .orElseThrow(() -> new TaskFactoryNotFoundException(factory)); + } + + private Map loadFactories() { + Map loadedFactories = new HashMap<>(); + ServiceLoader + .load(TaskFactory.class).iterator() + .forEachRemaining(f -> { + LOGGER.debug("Registering task factory [{}]", f.getName()); + loadedFactories.put(f.getName(), f); + }); + + return loadedFactories; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/action/InlineBodyActionFactory.java b/handler/core/src/main/java/io/knotx/fragments/handler/action/InlineBodyActionFactory.java index 5abbd31f..fe7b20ce 100644 --- a/handler/core/src/main/java/io/knotx/fragments/handler/action/InlineBodyActionFactory.java +++ b/handler/core/src/main/java/io/knotx/fragments/handler/action/InlineBodyActionFactory.java @@ -19,7 +19,7 @@ import io.knotx.fragments.handler.api.ActionFactory; import io.knotx.fragments.handler.api.Cacheable; import io.knotx.fragments.handler.api.domain.FragmentResult; -import io.knotx.fragments.task.options.GraphNodeOptions; +import io.knotx.fragments.task.factory.node.subtasks.SubtasksNodeFactory; import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; @@ -34,8 +34,8 @@ * } * } *
- * WARNING: This action modifies Fragment body so it should not be used in composite nodes - * {@link GraphNodeOptions#isComposite()}. + * WARNING: This action modifies Fragment body so it should not be used in subtasks nodes + * {@link SubtasksNodeFactory}. */ @Cacheable public class InlineBodyActionFactory implements ActionFactory { diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/exception/DoActionNotDefinedException.java b/handler/core/src/main/java/io/knotx/fragments/handler/exception/DoActionNotDefinedException.java index de3b1603..74881002 100644 --- a/handler/core/src/main/java/io/knotx/fragments/handler/exception/DoActionNotDefinedException.java +++ b/handler/core/src/main/java/io/knotx/fragments/handler/exception/DoActionNotDefinedException.java @@ -17,6 +17,8 @@ */ package io.knotx.fragments.handler.exception; +import io.knotx.fragments.ConfigurationException; + public class DoActionNotDefinedException extends ConfigurationException { public DoActionNotDefinedException(String message) { diff --git a/handler/core/src/main/java/io/knotx/fragments/task/ConfigurationTaskProviderFactory.java b/handler/core/src/main/java/io/knotx/fragments/handler/exception/TaskFactoryNameNotDefinedException.java similarity index 56% rename from handler/core/src/main/java/io/knotx/fragments/task/ConfigurationTaskProviderFactory.java rename to handler/core/src/main/java/io/knotx/fragments/handler/exception/TaskFactoryNameNotDefinedException.java index 23a33188..e686a937 100644 --- a/handler/core/src/main/java/io/knotx/fragments/task/ConfigurationTaskProviderFactory.java +++ b/handler/core/src/main/java/io/knotx/fragments/handler/exception/TaskFactoryNameNotDefinedException.java @@ -12,25 +12,22 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * - * The code comes from https://github.com/tomaszmichalak/vertx-rx-map-reduce. */ -package io.knotx.fragments.task; +package io.knotx.fragments.handler.exception; -import io.knotx.fragments.handler.action.ActionProvider; +import io.knotx.fragments.ConfigurationException; import io.vertx.core.json.JsonObject; -public class ConfigurationTaskProviderFactory implements TaskProviderFactory { +public class TaskFactoryNameNotDefinedException extends ConfigurationException { - public static final String NAME = "configuration"; + private JsonObject configuration; - @Override - public String getName() { - return NAME; + public TaskFactoryNameNotDefinedException(JsonObject configuration) { + super("Task factory name not defined [" + configuration + "]"); + this.configuration = configuration; } - @Override - public TaskProvider create(JsonObject config, ActionProvider actionProvider) { - return new ConfigurationTaskProvider(actionProvider); + public JsonObject getConfiguration() { + return configuration; } } diff --git a/handler/core/src/main/java/io/knotx/fragments/task/Configuration.java b/handler/core/src/main/java/io/knotx/fragments/handler/exception/TaskFactoryNotFoundException.java similarity index 56% rename from handler/core/src/main/java/io/knotx/fragments/task/Configuration.java rename to handler/core/src/main/java/io/knotx/fragments/handler/exception/TaskFactoryNotFoundException.java index df4fc4e6..c4980b7e 100644 --- a/handler/core/src/main/java/io/knotx/fragments/task/Configuration.java +++ b/handler/core/src/main/java/io/knotx/fragments/handler/exception/TaskFactoryNotFoundException.java @@ -13,26 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.knotx.fragments.task; +package io.knotx.fragments.handler.exception; -import io.knotx.fragments.task.options.GraphNodeOptions; +import io.knotx.fragments.ConfigurationException; -class Configuration { +public class TaskFactoryNotFoundException extends ConfigurationException { - private final String taskName; + private String factory; - private final GraphNodeOptions graphNodeOptions; - - Configuration(String taskName, GraphNodeOptions graphNodeOptions) { - this.taskName = taskName; - this.graphNodeOptions = graphNodeOptions; - } - - String getTaskName() { - return taskName; + public TaskFactoryNotFoundException(String factory) { + super("Task factory not found [" + factory + "]"); + this.factory = factory; } - GraphNodeOptions getGraphNodeOptions() { - return graphNodeOptions; + public String getFactory() { + return factory; } } diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/options/FragmentsHandlerOptions.java b/handler/core/src/main/java/io/knotx/fragments/handler/options/FragmentsHandlerOptions.java deleted file mode 100644 index fd236baa..00000000 --- a/handler/core/src/main/java/io/knotx/fragments/handler/options/FragmentsHandlerOptions.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2019 Knot.x Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.knotx.fragments.handler.options; - -import static io.knotx.fragments.handler.api.actionlog.ActionLogLevel.ERROR; - -import io.knotx.fragments.handler.action.ActionOptions; -import io.knotx.fragments.handler.api.actionlog.ActionLogLevel; -import io.knotx.fragments.task.options.TaskOptions; -import io.vertx.codegen.annotations.DataObject; -import io.vertx.core.json.JsonObject; -import java.util.Map; -import java.util.Objects; - -@DataObject(generateConverter = true) -public class FragmentsHandlerOptions { - - public static final String DEFAULT_TASK_KEY = "data-knotx-task"; - - private String taskKey; - - private Map tasks; - - private Map actions; - - private String logLevel; - - public FragmentsHandlerOptions(JsonObject json) { - init(json); - FragmentsHandlerOptionsConverter.fromJson(json, this); - } - - private void init(JsonObject json) { - this.taskKey = DEFAULT_TASK_KEY; - logLevel = json.getString(ActionLogLevel.CONFIG_KEY_NAME, ERROR.getLevel()); - } - - public JsonObject toJson() { - JsonObject jsonObject = new JsonObject(); - FragmentsHandlerOptionsConverter.toJson(this, jsonObject); - return jsonObject; - } - - public String getTaskKey() { - return taskKey; - } - - public FragmentsHandlerOptions setTaskKey(String taskKey) { - this.taskKey = taskKey; - return this; - } - - public Map getTasks() { - return tasks; - } - - /** - * Sets {@code Task} list, which are named, directed graphs of {@code Actions}. - * - * @param tasks list of defined {@code Tasks}. - * @return reference to this, so the API can be used fluently - */ - public FragmentsHandlerOptions setTasks(Map tasks) { - this.tasks = tasks; - return this; - } - - public Map getActions() { - return actions; - } - - /** - * Sets named actions with their factory configuration. - * - * @param actions list of named {@code Actions} (name -> Action) - * @return reference to this, so the API can be used fluently - */ - public FragmentsHandlerOptions setActions(Map actions) { - this.actions = actions; - return this; - } - - public String getLogLevel() { - return logLevel; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - FragmentsHandlerOptions that = (FragmentsHandlerOptions) o; - return Objects.equals(tasks, that.tasks) && - Objects.equals(actions, that.actions); - } - - @Override - public int hashCode() { - return Objects.hash(tasks, actions); - } - - @Override - public String toString() { - return "FragmentsHandlerOptions{" + - "tasks=" + tasks + - ", actions=" + actions + - ", actionLogMode=" + logLevel + - '}'; - } -} diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/package-info.java b/handler/core/src/main/java/io/knotx/fragments/package-info.java similarity index 95% rename from handler/core/src/main/java/io/knotx/fragments/handler/package-info.java rename to handler/core/src/main/java/io/knotx/fragments/package-info.java index ea0b3df9..599f294b 100644 --- a/handler/core/src/main/java/io/knotx/fragments/handler/package-info.java +++ b/handler/core/src/main/java/io/knotx/fragments/package-info.java @@ -14,6 +14,6 @@ * limitations under the License. */ @ModuleGen(name = "knotx-fragments-handler-core", groupPackage = "io.knotx") -package io.knotx.fragments.handler; +package io.knotx.fragments; import io.vertx.codegen.annotations.ModuleGen; \ No newline at end of file diff --git a/handler/core/src/main/java/io/knotx/fragments/task/ConfigurationTaskProvider.java b/handler/core/src/main/java/io/knotx/fragments/task/ConfigurationTaskProvider.java deleted file mode 100644 index 850b1812..00000000 --- a/handler/core/src/main/java/io/knotx/fragments/task/ConfigurationTaskProvider.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2019 Knot.x Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * The code comes from https://github.com/tomaszmichalak/vertx-rx-map-reduce. - */ -package io.knotx.fragments.task; - -import static io.knotx.fragments.handler.api.domain.FragmentResult.ERROR_TRANSITION; -import static io.knotx.fragments.handler.api.domain.FragmentResult.SUCCESS_TRANSITION; - -import io.knotx.fragments.engine.FragmentEventContext; -import io.knotx.fragments.engine.Task; -import io.knotx.fragments.engine.graph.SingleNode; -import io.knotx.fragments.engine.graph.CompositeNode; -import io.knotx.fragments.engine.graph.Node; -import io.knotx.fragments.handler.action.ActionProvider; -import io.knotx.fragments.handler.api.Action; -import io.knotx.fragments.handler.api.domain.FragmentContext; -import io.knotx.fragments.handler.api.domain.FragmentResult; -import io.knotx.fragments.task.exception.GraphConfigurationException; -import io.knotx.fragments.task.options.ActionNodeConfigOptions; -import io.knotx.fragments.task.options.SubtasksNodeConfigOptions; -import io.knotx.fragments.task.options.GraphNodeOptions; -import io.reactivex.Single; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; - -public class ConfigurationTaskProvider implements TaskProvider { - - private final ActionProvider actionProvider; - - ConfigurationTaskProvider(ActionProvider proxyProvider) { - this.actionProvider = proxyProvider; - } - - @Override - public Task newInstance(Configuration taskConfig, FragmentEventContext event) { - Node rootNode = initGraphRootNode(taskConfig.getGraphNodeOptions()); - return new Task(taskConfig.getTaskName(), rootNode); - } - - private Node initGraphRootNode(GraphNodeOptions options) { - Map transitions = options.getOnTransitions(); - Map edges = new HashMap<>(); - transitions.forEach((transition, childGraphOptions) -> { - edges.put(transition, initGraphRootNode(childGraphOptions)); - }); - final Node node; - if (options.isComposite()) { - node = buildCompositeNode(options, edges); - } else { - node = buildActionNode(options, edges); - } - return node; - } - - private Node buildActionNode(GraphNodeOptions options, Map edges) { - ActionNodeConfigOptions config = new ActionNodeConfigOptions(options.getNode().getConfig()); - Action action = actionProvider.get(config.getAction()).orElseThrow( - () -> new GraphConfigurationException("No provider for action " + config.getAction())); - return new SingleNode(config.getAction(), toRxFunction(action), edges); - } - - private Node buildCompositeNode(GraphNodeOptions options, Map edges) { - SubtasksNodeConfigOptions config = new SubtasksNodeConfigOptions( - options.getNode().getConfig()); - List nodes = config.getSubtasks().stream() - .map(this::initGraphRootNode) - .collect(Collectors.toList()); - return new CompositeNode(getNodeId(), nodes, edges.get(SUCCESS_TRANSITION), edges.get(ERROR_TRANSITION)); - } - - private String getNodeId() { - // TODO this value should be calculated based on graph, the behaviour now is not changed - return "composite"; - } - - private Function> toRxFunction( - Action action) { - io.knotx.fragments.handler.reactivex.api.Action rxAction = io.knotx.fragments.handler.reactivex.api.Action - .newInstance(action); - return rxAction::rxApply; - } - -} diff --git a/handler/core/src/main/java/io/knotx/fragments/task/TaskFactory.java b/handler/core/src/main/java/io/knotx/fragments/task/TaskFactory.java index a7c1fe6f..343a135b 100644 --- a/handler/core/src/main/java/io/knotx/fragments/task/TaskFactory.java +++ b/handler/core/src/main/java/io/knotx/fragments/task/TaskFactory.java @@ -12,83 +12,53 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * - * The code comes from https://github.com/tomaszmichalak/vertx-rx-map-reduce. */ package io.knotx.fragments.task; -import io.knotx.fragments.api.Fragment; import io.knotx.fragments.engine.FragmentEventContext; import io.knotx.fragments.engine.Task; -import io.knotx.fragments.handler.action.ActionProvider; -import io.knotx.fragments.task.exception.GraphConfigurationException; -import io.knotx.fragments.task.exception.TaskNotFoundException; -import io.knotx.fragments.task.options.TaskOptions; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.ServiceLoader; - -public class TaskFactory { - - private static final Logger LOGGER = LoggerFactory.getLogger(TaskFactory.class); - - private String taskKey; - private Map providersFactories; - private Map tasks; - private ActionProvider actionProvider; - - public TaskFactory(String taskKey, Map tasks, - ActionProvider actionProvider) { - this.taskKey = taskKey; - this.tasks = tasks; - this.actionProvider = actionProvider; - providersFactories = initProviders(); - } +import io.knotx.fragments.handler.FragmentsHandlerOptions; +import io.vertx.core.json.JsonObject; +import io.vertx.reactivex.core.Vertx; - public Optional newInstance(FragmentEventContext fragmentEventContext) { - return Optional.of(fragmentEventContext.getFragmentEvent().getFragment()) - .filter(this::hasTask) - .map(this::getTaskName) - .map(taskName -> { - TaskProvider factory = getProvider(taskName); - Configuration taskConfig = getTaskConfiguration(taskName); - return factory.newInstance(taskConfig, fragmentEventContext); - }); - } - - private String getTaskName(Fragment fragment) { - return fragment.getConfiguration().getString(taskKey); - } - - private boolean hasTask(Fragment fragment) { - return fragment.getConfiguration().containsKey(taskKey); - } +/** + * A task factory interface allowing to register a task factory by its name. Implementing class must + * be configured in META-INF.services. + * + * Task factories are configured in {@link FragmentsHandlerOptions#getTaskFactories()}. + */ +public interface TaskFactory { - private Configuration getTaskConfiguration(String taskName) { - return new Configuration(taskName, tasks.get(taskName).getGraph()); - } + /** + * @return task factory name + */ + String getName(); - private TaskProvider getProvider(String taskName) { - TaskOptions taskOptions = tasks.get(taskName); - if (taskOptions == null) { - LOGGER.error("Could not find task [{}] in tasks [{}]", taskName, tasks); - throw new TaskNotFoundException(taskName); - } - String factoryName = taskOptions.getFactory(); - return Optional.ofNullable(providersFactories.get(factoryName)) - .map(f -> f.create(taskOptions.getConfig(), actionProvider)) - .orElseThrow(() -> new GraphConfigurationException("Could not find task builder")); - } + /** + * Configures a task factory with config defined in {@link TaskFactoryOptions#getConfig()}. This + * method is called during factories initialization. + * + * @param config json task factory configuration, see {@link TaskFactoryOptions#getConfig()} + * @param vertx vertx instance + * @return a reference to this, so the API can be used fluently + */ + TaskFactory configure(JsonObject config, Vertx vertx); - private Map initProviders() { - Map factories = new HashMap<>(); - ServiceLoader - .load(TaskProviderFactory.class).iterator() - .forEachRemaining(f -> factories.put(f.getName(), f)); - return factories; - } + /** + * Determines if a fragment event context can be processed by the factory. + * + * @param context fragment event context + * @return true when accepted + */ + boolean accept(FragmentEventContext context); + /** + * Creates the new task instance. It is called only if {@link #accept(FragmentEventContext)} + * returns true. When called with a fragment that does not provide a task name, then + * {@link io.knotx.fragments.task.exception.TaskNotFoundException} is thrown. + * + * @param context fragment event context + * @return new task instance + */ + Task newInstance(FragmentEventContext context); } diff --git a/handler/core/src/main/java/io/knotx/fragments/task/TaskFactoryOptions.java b/handler/core/src/main/java/io/knotx/fragments/task/TaskFactoryOptions.java new file mode 100644 index 00000000..9f716ff4 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/task/TaskFactoryOptions.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task; + +import io.knotx.fragments.handler.exception.TaskFactoryNameNotDefinedException; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.core.json.JsonObject; +import java.util.Objects; +import org.apache.commons.lang3.StringUtils; + +/** + * Task Factory options model. It specifies task factory by its name and provides task factory + * config. + */ +@DataObject(generateConverter = true) +public class TaskFactoryOptions { + + private String factory; + private JsonObject config; + + public TaskFactoryOptions(JsonObject json) { + TaskFactoryOptionsConverter.fromJson(json, this); + if (StringUtils.isBlank(factory)) { + throw new TaskFactoryNameNotDefinedException(json); + } + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + TaskFactoryOptionsConverter.toJson(this, json); + return json; + } + + public String getFactory() { + return factory; + } + + /** + * The task factory name that identifies {@code TaskFactory} implementation. + * + * @param factory - task factory name + * @return reference to this, so the API can be used fluently + */ + public TaskFactoryOptions setFactory(String factory) { + this.factory = factory; + return this; + } + + public JsonObject getConfig() { + return config; + } + + /** + * The JSON object that contains task factory configuration entries. + * + * @param config - task factory config + * @return reference to this, so the API can be used fluently + */ + public TaskFactoryOptions setConfig(JsonObject config) { + this.config = config; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TaskFactoryOptions that = (TaskFactoryOptions) o; + return Objects.equals(factory, that.factory) && + Objects.equals(config, that.config); + } + + @Override + public int hashCode() { + return Objects.hash(factory, config); + } + + @Override + public String toString() { + return "TaskFactoryOptions{" + + "factory='" + factory + '\'' + + ", config=" + config + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/task/TaskProvider.java b/handler/core/src/main/java/io/knotx/fragments/task/TaskProvider.java deleted file mode 100644 index f29ba0a2..00000000 --- a/handler/core/src/main/java/io/knotx/fragments/task/TaskProvider.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2019 Knot.x Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.knotx.fragments.task; - -import io.knotx.fragments.engine.FragmentEventContext; -import io.knotx.fragments.engine.FragmentsEngine; -import io.knotx.fragments.engine.Task; - -/** - * Produces {@link Task} based on graph configuration, Fragment data and request. - */ -public interface TaskProvider { - - /** - * Produces Task that can be executed by {@link FragmentsEngine} - * - * @param config - task configuration - * @param event - contains Fragment, request - * @return configured task - */ - Task newInstance(Configuration config, FragmentEventContext event); - -} diff --git a/handler/core/src/main/java/io/knotx/fragments/task/exception/GraphConfigurationException.java b/handler/core/src/main/java/io/knotx/fragments/task/exception/NodeFactoryNotFoundException.java similarity index 64% rename from handler/core/src/main/java/io/knotx/fragments/task/exception/GraphConfigurationException.java rename to handler/core/src/main/java/io/knotx/fragments/task/exception/NodeFactoryNotFoundException.java index da081f25..81529fd0 100644 --- a/handler/core/src/main/java/io/knotx/fragments/task/exception/GraphConfigurationException.java +++ b/handler/core/src/main/java/io/knotx/fragments/task/exception/NodeFactoryNotFoundException.java @@ -12,16 +12,21 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * - * The code comes from https://github.com/tomaszmichalak/vertx-rx-map-reduce. */ package io.knotx.fragments.task.exception; -import io.knotx.fragments.handler.exception.ConfigurationException; +import io.knotx.fragments.ConfigurationException; + +public class NodeFactoryNotFoundException extends ConfigurationException { -public class GraphConfigurationException extends ConfigurationException { + private String factory; + + public NodeFactoryNotFoundException(String factory) { + super("Node factory not registered for [" + factory + "]"); + this.factory = factory; + } - public GraphConfigurationException(String message) { - super(message); + public String getFactory() { + return factory; } } diff --git a/handler/core/src/main/java/io/knotx/fragments/task/exception/TaskNotFoundException.java b/handler/core/src/main/java/io/knotx/fragments/task/exception/TaskNotFoundException.java index 7a484b73..df88b4f8 100644 --- a/handler/core/src/main/java/io/knotx/fragments/task/exception/TaskNotFoundException.java +++ b/handler/core/src/main/java/io/knotx/fragments/task/exception/TaskNotFoundException.java @@ -12,12 +12,10 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * - * The code comes from https://github.com/tomaszmichalak/vertx-rx-map-reduce. */ package io.knotx.fragments.task.exception; -import io.knotx.fragments.handler.exception.ConfigurationException; +import io.knotx.fragments.ConfigurationException; public class TaskNotFoundException extends ConfigurationException { @@ -25,6 +23,7 @@ public class TaskNotFoundException extends ConfigurationException { public TaskNotFoundException(String taskName) { super("Task [" + taskName + "] not configured!"); + this.taskName = taskName; } public String getTaskName() { diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/action/ActionOptions.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/ActionFactoryOptions.java similarity index 71% rename from handler/core/src/main/java/io/knotx/fragments/handler/action/ActionOptions.java rename to handler/core/src/main/java/io/knotx/fragments/task/factory/ActionFactoryOptions.java index 41c245ea..779efaef 100644 --- a/handler/core/src/main/java/io/knotx/fragments/handler/action/ActionOptions.java +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/ActionFactoryOptions.java @@ -13,38 +13,47 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.knotx.fragments.handler.action; - -import java.util.Objects; +package io.knotx.fragments.task.factory; import io.vertx.codegen.annotations.DataObject; import io.vertx.core.json.JsonObject; +import java.util.Objects; import java.util.Optional; @DataObject(generateConverter = true) -public class ActionOptions { +public class ActionFactoryOptions { private String factory; private JsonObject config; private String doAction; - ActionOptions(String factory, JsonObject config) { + private ActionFactoryOptions() { + } + + ActionFactoryOptions(String factory) { + this(factory, null, null); + } + + ActionFactoryOptions(String factory, JsonObject config) { this(factory, config, null); } - ActionOptions(String factory, JsonObject config, String doAction) { - this.factory = factory; - this.config = config; - this.doAction = doAction; + ActionFactoryOptions(String factory, JsonObject config, String doAction) { + ActionFactoryOptions actionFactoryOptions = new ActionFactoryOptions().setFactory(factory).setConfig(config) + .setDoAction(doAction); + + JsonObject json = new JsonObject(); + ActionFactoryOptionsConverter.toJson(actionFactoryOptions, json); + ActionFactoryOptionsConverter.fromJson(json, this); } - public ActionOptions(JsonObject json) { - ActionOptionsConverter.fromJson(json, this); + public ActionFactoryOptions(JsonObject json) { + ActionFactoryOptionsConverter.fromJson(json, this); } public JsonObject toJson() { JsonObject json = new JsonObject(); - ActionOptionsConverter.toJson(this, json); + ActionFactoryOptionsConverter.toJson(this, json); return json; } @@ -58,7 +67,7 @@ public String getFactory() { * @param factory action factory name. * @return reference to this, so the API can be used fluently */ - public ActionOptions setFactory(String factory) { + public ActionFactoryOptions setFactory(String factory) { this.factory = factory; return this; } @@ -73,7 +82,7 @@ public JsonObject getConfig() { * @param config action factory configuration. * @return reference to this, so the API can be used fluently */ - public ActionOptions setConfig(JsonObject config) { + public ActionFactoryOptions setConfig(JsonObject config) { this.config = config; return this; } @@ -89,7 +98,7 @@ public String getDoAction() { * @param doAction name of the base {@code Action}. * @return reference to this, so the API can be used fluently */ - public ActionOptions setDoAction(String doAction) { + public ActionFactoryOptions setDoAction(String doAction) { this.doAction = doAction; return this; } @@ -102,7 +111,7 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - ActionOptions that = (ActionOptions) o; + ActionFactoryOptions that = (ActionFactoryOptions) o; return Objects.equals(factory, that.factory) && Objects.equals(config, that.config) && Objects.equals(doAction, that.doAction); diff --git a/handler/core/src/main/java/io/knotx/fragments/task/factory/DefaultTaskFactory.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/DefaultTaskFactory.java new file mode 100644 index 00000000..795a0cd9 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/DefaultTaskFactory.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory; + +import io.knotx.fragments.api.Fragment; +import io.knotx.fragments.engine.FragmentEventContext; +import io.knotx.fragments.engine.Task; +import io.knotx.fragments.engine.graph.Node; +import io.knotx.fragments.task.TaskFactory; +import io.knotx.fragments.task.exception.NodeFactoryNotFoundException; +import io.knotx.fragments.task.exception.TaskNotFoundException; +import io.knotx.fragments.task.factory.node.NodeFactory; +import io.vertx.core.json.JsonObject; +import io.vertx.reactivex.core.Vertx; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public class DefaultTaskFactory implements TaskFactory, NodeProvider { + + public static final String NAME = "default"; + + private DefaultTaskFactoryConfig taskFactoryConfig; + private Map nodeFactories; + + @Override + public String getName() { + return NAME; + } + + @Override + public DefaultTaskFactory configure(JsonObject taskFactoryConfig, Vertx vertx) { + this.taskFactoryConfig = new DefaultTaskFactoryConfig(taskFactoryConfig); + nodeFactories = initFactories(vertx); + return this; + } + + @Override + public boolean accept(FragmentEventContext eventContext) { + Fragment fragment = eventContext.getFragmentEvent().getFragment(); + boolean fragmentContainsTask = fragment.getConfiguration() + .containsKey(taskFactoryConfig.getTaskNameKey()); + return fragmentContainsTask && isTaskConfigured(fragment); + } + + private boolean isTaskConfigured(Fragment fragment) { + String taskName = fragment.getConfiguration().getString(taskFactoryConfig.getTaskNameKey()); + return taskFactoryConfig.getTasks().containsKey(taskName); + } + + @Override + public Task newInstance(FragmentEventContext eventContext) { + Fragment fragment = eventContext.getFragmentEvent().getFragment(); + String taskKey = taskFactoryConfig.getTaskNameKey(); + String taskName = fragment.getConfiguration().getString(taskKey); + + Map tasks = taskFactoryConfig.getTasks(); + return Optional.ofNullable(tasks.get(taskName)) + .map(rootGraphNodeOptions -> { + Node rootNode = initNode(rootGraphNodeOptions); + return new Task(taskName, rootNode); + }) + .orElseThrow(() -> new TaskNotFoundException(taskName)); + } + + @Override + public Node initNode(GraphNodeOptions nodeOptions) { + return findNodeFactory(nodeOptions) + .map(f -> f.initNode(nodeOptions, initTransitions(nodeOptions), this)) + .orElseThrow(() -> new NodeFactoryNotFoundException(nodeOptions.getNode().getFactory())); + } + + private Optional findNodeFactory(GraphNodeOptions nodeOptions) { + return Optional.ofNullable(nodeFactories.get(nodeOptions.getNode().getFactory())); + } + + private Map initTransitions(GraphNodeOptions nodeOptions) { + Map transitions = nodeOptions.getOnTransitions(); + Map edges = new HashMap<>(); + transitions.forEach((transition, childGraphOptions) -> edges + .put(transition, initNode(childGraphOptions))); + return edges; + } + + private Map initFactories(Vertx vertx) { + ServiceLoader factories = ServiceLoader.load(NodeFactory.class); + return taskFactoryConfig.getNodeFactories().stream() + .map(options -> { + NodeFactory factory = findNodeFactory(factories, options.getFactory()); + return factory.configure(options.getConfig(), vertx); + }).collect(Collectors.toMap(NodeFactory::getName, f -> f)); + } + + private NodeFactory findNodeFactory(ServiceLoader factories, String factory) { + Stream factoryStream = StreamSupport.stream( + Spliterators.spliteratorUnknownSize(factories.iterator(), Spliterator.ORDERED), + false); + + return factoryStream + .filter(f -> f.getName().equals(factory)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Node not defined")); + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/task/factory/DefaultTaskFactoryConfig.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/DefaultTaskFactoryConfig.java new file mode 100644 index 00000000..72878add --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/DefaultTaskFactoryConfig.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory; + +import io.knotx.fragments.task.factory.node.NodeFactoryOptions; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.core.json.JsonObject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.apache.commons.lang3.StringUtils; + +/** + * Default Task Factory config model. + */ +@DataObject(generateConverter = true) +public class DefaultTaskFactoryConfig { + + public static final String DEFAULT_TASK_NAME_KEY = "data-knotx-task"; + + private Map tasks; + private List nodeFactories; + private String taskNameKey; + private String logLevel; + + public DefaultTaskFactoryConfig() { + tasks = new HashMap<>(); + nodeFactories = new ArrayList<>(); + taskNameKey = DEFAULT_TASK_NAME_KEY; + } + + public DefaultTaskFactoryConfig(JsonObject json) { + this(); + DefaultTaskFactoryConfigConverter.fromJson(json, this); + initNodeLogLevel(json); + } + + private void initNodeLogLevel(JsonObject json) { + LogLevelConfig globalLogLevel = new LogLevelConfig(json); + if (StringUtils.isNotBlank(logLevel)) { + nodeFactories.forEach(nodeFactoryOptions -> + override(nodeFactoryOptions.getConfig(), globalLogLevel.getLogLevel())); + } + } + + private void override(JsonObject json, String globalLogLevel) { + if (!StringUtils.isBlank(globalLogLevel)) { + LogLevelConfig logLevelConfig = new LogLevelConfig(json); + if (StringUtils.isBlank(logLevelConfig.getLogLevel())) { + json.mergeIn(logLevelConfig.setLogLevel(globalLogLevel).toJson()); + } + } + } + + public JsonObject toJson() { + JsonObject result = new JsonObject(); + DefaultTaskFactoryConfigConverter.toJson(this, result); + return result; + } + + + public Map getTasks() { + return tasks; + } + + /** + * The dictionary that maps a task name to a directed acyclic graph (DAG) of nodes. + * + * @param tasks - map that links task name with its graph logic + * @return reference to this, so the API can be used fluently + */ + public DefaultTaskFactoryConfig setTasks(Map tasks) { + this.tasks = tasks; + return this; + } + + public List getNodeFactories() { + return nodeFactories; + } + + /** + * The array/list of graph node factory options defines node factories taking part in the creation + * of graph. + * + * @param nodeFactories - list of graph node factory options + * @return reference to this, so the API can be used fluently + */ + public DefaultTaskFactoryConfig setNodeFactories(List nodeFactories) { + this.nodeFactories = nodeFactories; + return this; + } + + public String getTaskNameKey() { + return taskNameKey; + } + + /** + * The fragment's configuration key specifies a task assigned to a fragment by the task name. + * + * @param taskNameKey - fragment's configuration key + * @return reference to this, so the API can be used fluently + */ + public DefaultTaskFactoryConfig setTaskNameKey(String taskNameKey) { + this.taskNameKey = taskNameKey; + return this; + } + + + public String getLogLevel() { + return logLevel; + } + + /** + * The global node log level. + * + * @param logLevel - node log level + * @return reference to this, so the API can be used fluently + */ + public DefaultTaskFactoryConfig setLogLevel(String logLevel) { + this.logLevel = logLevel; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultTaskFactoryConfig that = (DefaultTaskFactoryConfig) o; + return Objects.equals(tasks, that.tasks) && + Objects.equals(nodeFactories, that.nodeFactories) && + Objects.equals(taskNameKey, that.taskNameKey); + } + + @Override + public int hashCode() { + return Objects.hash(tasks, nodeFactories, taskNameKey); + } + + @Override + public String toString() { + return "DefaultTaskFactoryConfig{" + + "tasks=" + tasks + + ", nodeFactories=" + nodeFactories + + ", taskNameKey='" + taskNameKey + '\'' + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/task/options/GraphNodeOptions.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/GraphNodeOptions.java similarity index 58% rename from handler/core/src/main/java/io/knotx/fragments/task/options/GraphNodeOptions.java rename to handler/core/src/main/java/io/knotx/fragments/task/factory/GraphNodeOptions.java index 0c492ce8..99beacda 100644 --- a/handler/core/src/main/java/io/knotx/fragments/task/options/GraphNodeOptions.java +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/GraphNodeOptions.java @@ -13,8 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.knotx.fragments.task.options; +package io.knotx.fragments.task.factory; +import io.knotx.fragments.task.factory.node.NodeOptions; +import io.knotx.fragments.task.factory.node.action.ActionNodeConfig; +import io.knotx.fragments.task.factory.node.action.ActionNodeFactory; +import io.knotx.fragments.task.factory.node.subtasks.SubtasksNodeConfig; +import io.knotx.fragments.task.factory.node.subtasks.SubtasksNodeFactory; import io.vertx.codegen.annotations.DataObject; import io.vertx.core.json.JsonObject; import java.util.Collections; @@ -24,44 +29,19 @@ import java.util.Optional; /** - * It is {@link io.knotx.fragments.engine.Task} processing configuration. Task is graph of nodes (in - * fact it is tree structure). It defines {@link NodeOptions} and outgoing directed graph edges, - * called {@code Transitions}. - * - * It represents JSON configuration: - *
- * {
- *   node = {
- *     factory = action
- *     config {
- *       action = a
- *     }
- *   }
- *   onTransitions {
- *     _success {
- *       node = {
- *         factory = action
- *         config {
- *           action = b
- *         }
- *       }
- *     }
- *   }
- * }
- * 
- * - * Please note that Transitions define next graph nodes. + * Graph node options model. */ @DataObject(generateConverter = true) public class GraphNodeOptions { - // TODO move to node factories - public static final String SUBTASKS = "subtasks"; - public static final String ACTION = "action"; - private NodeOptions node; private Map onTransitions; + public GraphNodeOptions(NodeOptions nodeOptions, Map transitions) { + this.node = nodeOptions; + this.onTransitions = transitions; + } + public GraphNodeOptions(String action, Map transitions) { init(); setAction(action); @@ -93,19 +73,14 @@ public JsonObject toJson() { return result; } - /** - * Gets node options. - * - * @return node options - */ public NodeOptions getNode() { return node; } /** - * Sets node options defining node factory and its configuration. + * Node options define a node factory and its configuration. * - * @param node node options + * @param node - node options * @return reference to this, so the API can be used fluently */ public GraphNodeOptions setNode(NodeOptions node) { @@ -114,29 +89,25 @@ public GraphNodeOptions setNode(NodeOptions node) { } /** - * Gets next graph node for given Transition. If Transition is not configured then {@link - * Optional#empty()} is returned. + * It specifies the next graph node for the given transition. If no graph edge is defined, then an + * empty value is returned. * - * @param transition transition + * @param transition - non blank transition + * @return the next node options if defined */ public Optional get(String transition) { return Optional.ofNullable(onTransitions.get(transition)); } - /** - * Gets Transition to next graph node map. - * - * @return Transition to graph node map - */ public Map getOnTransitions() { return onTransitions; } /** - * Sets outgoing graph node edges, called {@code Transitions}. Transition is String, {@code - * onTransitions} map links Transition with next graph node. + * The outgoing graph node edges, called transitions. A transition is named graph edge that + * defines the next graph node in fragment's processing. * - * @param onTransitions map of possible transitions. + * @param onTransitions - map of possible transitions. * @return reference to this, so the API can be used fluently */ public GraphNodeOptions setOnTransitions(Map onTransitions) { @@ -145,37 +116,45 @@ public GraphNodeOptions setOnTransitions(Map onTransit } /** - * @see ActionNodeConfigOptions#setAction(String) + * Sets a node factory name to {@code ActionNodeFactory.NAME} and configures the action. + * + * @param action - action name for action node config + * @return reference to this, so the API can be used fluently + * @see ActionNodeFactory#NAME */ - public GraphNodeOptions setAction(String actionName) { - node.setFactory(ACTION); - node.setConfig(new ActionNodeConfigOptions(actionName).toJson()); + public GraphNodeOptions setAction(String action) { + node.setFactory(ActionNodeFactory.NAME); + node.setConfig(new ActionNodeConfig(action).toJson()); return this; } /** - * @see SubtasksNodeConfigOptions#setSubtasks(List) + * Sets a node factory name to {@code SubtasksNodeFactory.NAME} and configures subgraphs. + * + * @param subtasks - list of subtasks (subgraphs) options + * @return reference to this, so the API can be used fluently + * @see SubtasksNodeFactory#NAME + * @deprecated use {@link #setSubtasks(List)} */ @Deprecated - public GraphNodeOptions setActions(List subTasks) { - setSubtasks(subTasks); + public GraphNodeOptions setActions(List subtasks) { + setSubtasks(subtasks); return this; } /** - * @see SubtasksNodeConfigOptions#setSubtasks(List) + * Sets a node factory name to {@code SubtasksNodeFactory.NAME} and configures subgraphs. + * + * @param subtasks - list of subtasks (subgraphs) options + * @return reference to this, so the API can be used fluently + * @see SubtasksNodeFactory#NAME */ public GraphNodeOptions setSubtasks(List subtasks) { - node.setFactory(SUBTASKS); - node.setConfig(new SubtasksNodeConfigOptions(subtasks).toJson()); + node.setFactory(SubtasksNodeFactory.NAME); + node.setConfig(new SubtasksNodeConfig(subtasks).toJson()); return this; } - // TODO remove when node factories finished - public boolean isComposite() { - return SUBTASKS.equals(node.getFactory()); - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/handler/core/src/main/java/io/knotx/fragments/task/factory/LogLevelConfig.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/LogLevelConfig.java new file mode 100644 index 00000000..c0da82f7 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/LogLevelConfig.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.core.json.JsonObject; +import java.util.Objects; +import org.apache.commons.lang3.StringUtils; + +@DataObject(generateConverter = true) +public class LogLevelConfig { + + private String logLevel; + + public LogLevelConfig(JsonObject json) { + LogLevelConfigConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject jsonObject = new JsonObject(); + LogLevelConfigConverter.toJson(this, jsonObject); + return jsonObject; + } + + public static JsonObject override(JsonObject json, String defaultLogLevel) { + if (!StringUtils.isBlank(defaultLogLevel)) { + LogLevelConfig logLevelConfig = new LogLevelConfig(json); + if (StringUtils.isBlank(logLevelConfig.getLogLevel())) { + json.mergeIn(logLevelConfig.setLogLevel(defaultLogLevel).toJson()); + } + } + return json; + } + + public String getLogLevel() { + return logLevel; + } + + public LogLevelConfig setLogLevel(String logLevel) { + this.logLevel = logLevel; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LogLevelConfig that = (LogLevelConfig) o; + return Objects.equals(logLevel, that.logLevel); + } + + @Override + public int hashCode() { + return Objects.hash(logLevel); + } + + @Override + public String toString() { + return "LogLevelConfig{" + + "logLevel='" + logLevel + '\'' + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/task/TaskProviderFactory.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/NodeProvider.java similarity index 70% rename from handler/core/src/main/java/io/knotx/fragments/task/TaskProviderFactory.java rename to handler/core/src/main/java/io/knotx/fragments/task/factory/NodeProvider.java index bfd8b907..2d8cd315 100644 --- a/handler/core/src/main/java/io/knotx/fragments/task/TaskProviderFactory.java +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/NodeProvider.java @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.knotx.fragments.task; +package io.knotx.fragments.task.factory; -import io.knotx.fragments.handler.action.ActionProvider; -import io.vertx.core.json.JsonObject; +import io.knotx.fragments.engine.graph.Node; -public interface TaskProviderFactory { - - String getName(); +/** + * Inits node based on node options. + */ +public interface NodeProvider { - TaskProvider create(JsonObject config, ActionProvider proxyProvider); + Node initNode(GraphNodeOptions nodeOptions); } diff --git a/handler/core/src/main/java/io/knotx/fragments/task/factory/node/NodeFactory.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/NodeFactory.java new file mode 100644 index 00000000..0fa9c087 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/NodeFactory.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory.node; + +import io.knotx.fragments.engine.graph.Node; +import io.knotx.fragments.task.factory.GraphNodeOptions; +import io.knotx.fragments.task.factory.NodeProvider; +import io.vertx.core.json.JsonObject; +import io.vertx.reactivex.core.Vertx; +import java.util.Map; + +/** + * A node factory interface allowing to register a node factory by its name. Implementing class must + * be configured in META-INF.services. + * + * Node factories are configured in {@link io.knotx.fragments.task.factory.DefaultTaskFactory#configure(JsonObject, + * Vertx)}. + */ +public interface NodeFactory { + + /** + * @return node factory name + */ + String getName(); + + /** + * Configures a node factory with config defined in {@link NodeFactoryOptions#getConfig()}. This + * method is called during factories initialization. + * + * @param config - json node factory configuration, see {@link NodeFactoryOptions#getConfig()} + * @param vertx - vertx instance + * @return a reference to this, so the API can be used fluently + * @see NodeFactoryOptions#getConfig() + */ + NodeFactory configure(JsonObject config, Vertx vertx); + + /** + * Initialize node instance. Nodes are stateless and stateful. + * + * @param nodeOptions - graph node options + * @param edges - prepared node outgoing edges + * @param nodeProvider - node provider if the current node contains others + * @return node instance + */ + Node initNode(GraphNodeOptions nodeOptions, Map edges, NodeProvider nodeProvider); + +} diff --git a/handler/core/src/main/java/io/knotx/fragments/task/options/TaskOptions.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/NodeFactoryOptions.java similarity index 52% rename from handler/core/src/main/java/io/knotx/fragments/task/options/TaskOptions.java rename to handler/core/src/main/java/io/knotx/fragments/task/factory/node/NodeFactoryOptions.java index a2cef51a..2fad9c59 100644 --- a/handler/core/src/main/java/io/knotx/fragments/task/options/TaskOptions.java +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/NodeFactoryOptions.java @@ -13,59 +13,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.knotx.fragments.task.options; +package io.knotx.fragments.task.factory.node; -import io.knotx.fragments.task.ConfigurationTaskProviderFactory; -import io.knotx.fragments.task.TaskProviderFactory; +import io.knotx.fragments.task.factory.node.action.ActionProvider; import io.vertx.codegen.annotations.DataObject; import io.vertx.core.json.JsonObject; import java.util.Objects; /** - * Task options. + * Node Factory options model. */ @DataObject(generateConverter = true) -public class TaskOptions { +public class NodeFactoryOptions { private String factory; private JsonObject config; - private GraphNodeOptions graph; - public TaskOptions(JsonObject json) { - init(); - TaskOptionsConverter.fromJson(json, this); - if (graph == null) { - graph = new GraphNodeOptions(json); - } + public NodeFactoryOptions() { + config = new JsonObject(); } - private void init() { - factory = ConfigurationTaskProviderFactory.NAME; - config = new JsonObject(); + public NodeFactoryOptions(JsonObject json) { + this(); + NodeFactoryOptionsConverter.fromJson(json, this); } public JsonObject toJson() { JsonObject result = new JsonObject(); - TaskOptionsConverter.toJson(this, result); + NodeFactoryOptionsConverter.toJson(this, result); return result; } - /** - * Gets {@link TaskProviderFactory} name - * - * @return task provider factory name - */ public String getFactory() { return factory; } /** - * Sets task provider factory name + * The node factory name that identifies {@code NodeFactory} implementation. * - * @param factory - task provider factory name + * @param factory - node factory name * @return reference to this, so the API can be used fluently */ - public TaskOptions setFactory(String factory) { + public NodeFactoryOptions setFactory(String factory) { this.factory = factory; return this; } @@ -75,36 +64,16 @@ public JsonObject getConfig() { } /** - * Gets task provider factory configuration. + * The JSON object that contains node factory configuration entries. * - * @param config task provider factory configuration + * @param config - node factory config * @return reference to this, so the API can be used fluently */ - public TaskOptions setConfig(JsonObject config) { + public NodeFactoryOptions setConfig(JsonObject config) { this.config = config; return this; } - /** - * Gets task graph of executable nodes. - * - * @return graph of nodes - */ - public GraphNodeOptions getGraph() { - return graph; - } - - /** - * Sets task graph. - * - * @param graph - graph of nodes - * @return reference to this, so the API can be used fluently - */ - public TaskOptions setGraph(GraphNodeOptions graph) { - this.graph = graph; - return this; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -113,23 +82,21 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - TaskOptions that = (TaskOptions) o; + NodeFactoryOptions that = (NodeFactoryOptions) o; return Objects.equals(factory, that.factory) && - Objects.equals(config, that.config) && - Objects.equals(graph, that.graph); + Objects.equals(config, that.config); } @Override public int hashCode() { - return Objects.hash(factory, config, graph); + return Objects.hash(factory, config); } @Override public String toString() { - return "TaskOptions{" + + return "NodeFactoryOptions{" + "factory='" + factory + '\'' + ", config=" + config + - ", graph=" + graph + '}'; } } diff --git a/handler/core/src/main/java/io/knotx/fragments/task/options/NodeOptions.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/NodeOptions.java similarity index 86% rename from handler/core/src/main/java/io/knotx/fragments/task/options/NodeOptions.java rename to handler/core/src/main/java/io/knotx/fragments/task/factory/node/NodeOptions.java index 88355d98..09050fd2 100644 --- a/handler/core/src/main/java/io/knotx/fragments/task/options/NodeOptions.java +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/NodeOptions.java @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.knotx.fragments.task.options; +package io.knotx.fragments.task.factory.node; +import io.knotx.fragments.task.factory.node.action.ActionNodeConfig; +import io.knotx.fragments.task.factory.node.subtasks.SubtasksNodeConfig; import io.vertx.codegen.annotations.DataObject; import io.vertx.core.json.JsonObject; import java.util.Objects; @@ -28,17 +30,19 @@ public class NodeOptions { private String factory; private JsonObject config; - NodeOptions() { - init(); + public NodeOptions() { + config = new JsonObject(); } - public NodeOptions(JsonObject json) { - init(); - NodeOptionsConverter.fromJson(json, this); + public NodeOptions(String factory, JsonObject config) { + this(); + this.factory = factory; + this.config = config; } - private void init() { - config = new JsonObject(); + public NodeOptions(JsonObject json) { + this(); + NodeOptionsConverter.fromJson(json, this); } public JsonObject toJson() { @@ -70,8 +74,8 @@ public NodeOptions setFactory(String factory) { /** * Gets node configuration. The default ones are: *
-   * - {@link ActionNodeConfigOptions}
-   * - {@link SubtasksNodeConfigOptions}
+   * - {@link ActionNodeConfig}
+   * - {@link SubtasksNodeConfig}
    * 
* * @return JSON representation of above config options diff --git a/handler/core/src/main/java/io/knotx/fragments/task/options/ActionNodeConfigOptions.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionNodeConfig.java similarity index 80% rename from handler/core/src/main/java/io/knotx/fragments/task/options/ActionNodeConfigOptions.java rename to handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionNodeConfig.java index 718b8f1c..c8df5b2c 100644 --- a/handler/core/src/main/java/io/knotx/fragments/task/options/ActionNodeConfigOptions.java +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionNodeConfig.java @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.knotx.fragments.task.options; +package io.knotx.fragments.task.factory.node.action; +import io.knotx.fragments.task.factory.node.NodeOptions; import io.vertx.codegen.annotations.DataObject; import io.vertx.core.json.JsonObject; import java.util.Objects; @@ -25,29 +26,28 @@ *
  * node {
  *   factory = action
- *   config { //represented by ActionNodeConfigOptions
+ *   config { //represented by ActionNodeConfig
  *     ...
  *   }
  * }
  * 
*/ @DataObject(generateConverter = true) -public class ActionNodeConfigOptions { +public class ActionNodeConfig { private String action; - - ActionNodeConfigOptions(String action) { + public ActionNodeConfig(String action) { setAction(action); } - public ActionNodeConfigOptions(JsonObject json) { - ActionNodeConfigOptionsConverter.fromJson(json, this); + public ActionNodeConfig(JsonObject json) { + ActionNodeConfigConverter.fromJson(json, this); } public JsonObject toJson() { JsonObject json = new JsonObject(); - ActionNodeConfigOptionsConverter.toJson(this, json); + ActionNodeConfigConverter.toJson(this, json); return json; } @@ -68,7 +68,7 @@ public String getAction() { * @param action action name * @return reference to this, so the API can be used fluently */ - public ActionNodeConfigOptions setAction(String action) { + public ActionNodeConfig setAction(String action) { this.action = action; return this; } @@ -81,7 +81,7 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - ActionNodeConfigOptions that = (ActionNodeConfigOptions) o; + ActionNodeConfig that = (ActionNodeConfig) o; return Objects.equals(action, that.action); } diff --git a/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionNodeFactory.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionNodeFactory.java new file mode 100644 index 00000000..ebd5728c --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionNodeFactory.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory.node.action; + +import io.knotx.fragments.engine.graph.Node; +import io.knotx.fragments.engine.graph.SingleNode; +import io.knotx.fragments.handler.api.Action; +import io.knotx.fragments.handler.api.ActionFactory; +import io.knotx.fragments.handler.api.domain.FragmentContext; +import io.knotx.fragments.handler.api.domain.FragmentResult; +import io.knotx.fragments.task.factory.NodeProvider; +import io.knotx.fragments.task.factory.node.NodeFactory; +import io.knotx.fragments.task.factory.GraphNodeOptions; +import io.reactivex.Single; +import io.vertx.core.json.JsonObject; +import io.vertx.reactivex.core.Vertx; +import java.util.Iterator; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.function.Function; +import java.util.function.Supplier; + +public class ActionNodeFactory implements NodeFactory { + + public static final String NAME = "action"; + private ActionProvider actionProvider; + + @Override + public String getName() { + return NAME; + } + + @Override + public ActionNodeFactory configure(JsonObject config, Vertx vertx) { + actionProvider = new ActionProvider(supplyFactories(), + new ActionNodeFactoryConfig(config).getActions(), vertx); + return this; + } + + @Override + public Node initNode(GraphNodeOptions nodeOptions, Map edges, + NodeProvider nodeProvider) { + ActionNodeConfig config = new ActionNodeConfig(nodeOptions.getNode().getConfig()); + Action action = actionProvider.get(config.getAction()).orElseThrow( + () -> new ActionNotFoundException(config.getAction())); + return new SingleNode(config.getAction(), toRxFunction(action), edges); + } + + private Function> toRxFunction( + Action action) { + io.knotx.fragments.handler.reactivex.api.Action rxAction = io.knotx.fragments.handler.reactivex.api.Action + .newInstance(action); + return rxAction::rxApply; + } + + private Supplier> supplyFactories() { + return () -> { + ServiceLoader factories = ServiceLoader + .load(ActionFactory.class); + return factories.iterator(); + }; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionNodeFactoryConfig.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionNodeFactoryConfig.java new file mode 100644 index 00000000..a998fe47 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionNodeFactoryConfig.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory.node.action; + +import io.knotx.fragments.task.factory.ActionFactoryOptions; +import io.knotx.fragments.task.factory.LogLevelConfig; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.core.json.JsonObject; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Action Node factory config model. + */ +@DataObject(generateConverter = true) +public class ActionNodeFactoryConfig { + + private Map actions; + + public ActionNodeFactoryConfig(Map actions) { + this.actions = actions; + } + + public ActionNodeFactoryConfig(JsonObject json) { + actions = new HashMap<>(); + ActionNodeFactoryConfigConverter.fromJson(json, this); + initActionLogLevel(json); + } + + private void initActionLogLevel(JsonObject json) { + LogLevelConfig globalLogLevel = new LogLevelConfig(json); + actions.values().forEach(actionOptions -> { + JsonObject actionConfig = actionOptions.getConfig(); + LogLevelConfig.override(actionConfig, globalLogLevel.getLogLevel()); + }); + } + + public JsonObject toJson() { + JsonObject jsonObject = new JsonObject(); + ActionNodeFactoryConfigConverter.toJson(this, jsonObject); + return jsonObject; + } + + public Map getActions() { + return actions; + } + + /** + * The dictionary maps action name to action factory options. + * + * @param actions map of actions + * @return reference to this, so the API can be used fluently + * @see ActionProvider#get(String) + */ + public ActionNodeFactoryConfig setActions(Map actions) { + this.actions = actions; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ActionNodeFactoryConfig that = (ActionNodeFactoryConfig) o; + return Objects.equals(actions, that.actions); + } + + @Override + public int hashCode() { + return Objects.hash(actions); + } + + @Override + public String toString() { + return "ActionsConfig{" + + "actions=" + actions + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionNotFoundException.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionNotFoundException.java new file mode 100644 index 00000000..6d20438f --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionNotFoundException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory.node.action; + +import io.knotx.fragments.ConfigurationException; + +public class ActionNotFoundException extends ConfigurationException { + + private String action; + + public ActionNotFoundException(String action) { + super("Action not configured [" + action + "]"); + this.action = action; + } + + public String getAction() { + return action; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/action/ActionProvider.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionProvider.java similarity index 59% rename from handler/core/src/main/java/io/knotx/fragments/handler/action/ActionProvider.java rename to handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionProvider.java index 65c5967f..f1e37d52 100644 --- a/handler/core/src/main/java/io/knotx/fragments/handler/action/ActionProvider.java +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionProvider.java @@ -13,17 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.knotx.fragments.handler.action; +package io.knotx.fragments.task.factory.node.action; -import static io.knotx.fragments.handler.api.actionlog.ActionLogLevel.CONFIG_KEY_NAME; - -import io.knotx.fragments.handler.api.Cacheable; +import io.knotx.fragments.task.factory.ActionFactoryOptions; import io.knotx.fragments.handler.api.Action; import io.knotx.fragments.handler.api.ActionFactory; -import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; +import io.knotx.fragments.handler.api.Cacheable; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; +import io.vertx.reactivex.core.Vertx; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -36,19 +34,17 @@ public class ActionProvider { private static final Logger LOGGER = LoggerFactory.getLogger(ActionProvider.class); - private final Map options; - private final Vertx vertx; - private final Map factories; private final Map cache; - private final String globalActionLogLevel; - public ActionProvider(Map options, - Supplier> factoriesSupplier, String globalActionLogLevel, Vertx vertx) { - this.options = options; + private Map actionNameToOptions; + private Vertx vertx; + + public ActionProvider(Supplier> supplier, + Map actionNameToOptions, Vertx vertx) { + this.actionNameToOptions = actionNameToOptions; this.vertx = vertx; - this.factories = loadFactories(factoriesSupplier); - this.globalActionLogLevel = globalActionLogLevel; + this.factories = loadFactories(supplier); this.cache = new HashMap<>(); } @@ -56,46 +52,38 @@ public Optional get(String action) { if (StringUtils.isBlank(action)) { return Optional.empty(); } - ActionOptions actionOptions = options.get(action); - if (actionOptions == null) { + ActionFactoryOptions actionFactoryOptions = actionNameToOptions.get(action); + if (actionFactoryOptions == null) { LOGGER.warn("Could not create initialize proxy [{}] with missing config.", action); return Optional.empty(); } - ActionFactory factory = factories.get(actionOptions.getFactory()); + ActionFactory factory = factories.get(actionFactoryOptions.getFactory()); if (factory == null) { LOGGER.warn("Could not create initialize proxy [{}] with missing factory [{}].", action, - actionOptions.getFactory()); + actionFactoryOptions.getFactory()); return Optional.empty(); } if (isCacheable(factory)) { - return Optional.of(cache.computeIfAbsent(action, toAction(actionOptions, factory))); + return Optional.of(cache.computeIfAbsent(action, toAction(actionFactoryOptions, factory))); } else { - return Optional.of(createAction(action, actionOptions, factory)); + return Optional.of(createAction(action, actionFactoryOptions, factory)); } } - private Function toAction(ActionOptions actionOptions, ActionFactory factory) { - return action -> createAction(action, actionOptions, factory); + private Function toAction(ActionFactoryOptions actionFactoryOptions, + ActionFactory factory) { + return action -> createAction(action, actionFactoryOptions, factory); } - private Action createAction(String action, ActionOptions actionOptions, ActionFactory factory){ + private Action createAction(String action, ActionFactoryOptions actionFactoryOptions, + ActionFactory factory) { // recurrence here :) - Action operation = Optional.ofNullable(actionOptions.getDoAction()) + Action operation = Optional.ofNullable(actionFactoryOptions.getDoAction()) .flatMap(this::get) .orElse(null); - return factory.create(action, prepareActionConfig(actionOptions), vertx, operation); - } - - private JsonObject prepareActionConfig(ActionOptions actionOptions){ - JsonObject config = actionOptions.getConfig(); - - if(config.fieldNames().contains(CONFIG_KEY_NAME)){ - return config; - } - - return config.put(CONFIG_KEY_NAME, globalActionLogLevel); + return factory.create(action, actionFactoryOptions.getConfig(), vertx.getDelegate(), operation); } private boolean isCacheable(ActionFactory factory) { diff --git a/handler/core/src/main/java/io/knotx/fragments/task/options/SubtasksNodeConfigOptions.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/subtasks/SubtasksNodeConfig.java similarity index 64% rename from handler/core/src/main/java/io/knotx/fragments/task/options/SubtasksNodeConfigOptions.java rename to handler/core/src/main/java/io/knotx/fragments/task/factory/node/subtasks/SubtasksNodeConfig.java index 928c4759..94feac35 100644 --- a/handler/core/src/main/java/io/knotx/fragments/task/options/SubtasksNodeConfigOptions.java +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/subtasks/SubtasksNodeConfig.java @@ -13,41 +13,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.knotx.fragments.task.options; +package io.knotx.fragments.task.factory.node.subtasks; +import io.knotx.fragments.task.factory.GraphNodeOptions; import io.vertx.codegen.annotations.DataObject; import io.vertx.core.json.JsonObject; import java.util.List; import java.util.Objects; /** - * Subtask node configuration. It is model for {@link NodeOptions#getConfig()} JSON object. - * - *
- * node {
- *   factory = subtasks
- *   config { //represented by SubtasksNodeConfigOptions
- *     ...
- *   }
- * }
- * 
+ * Subtask Node configuration. */ @DataObject(generateConverter = true) -public class SubtasksNodeConfigOptions { +public class SubtasksNodeConfig { private List subtasks; - public SubtasksNodeConfigOptions(List subtasks) { + public SubtasksNodeConfig(List subtasks) { this.subtasks = subtasks; } - public SubtasksNodeConfigOptions(JsonObject json) { - SubtasksNodeConfigOptionsConverter.fromJson(json, this); + public SubtasksNodeConfig(JsonObject json) { + SubtasksNodeConfigConverter.fromJson(json, this); } public JsonObject toJson() { JsonObject json = new JsonObject(); - SubtasksNodeConfigOptionsConverter.toJson(this, json); + SubtasksNodeConfigConverter.toJson(this, json); return json; } @@ -56,13 +48,12 @@ public List getSubtasks() { } /** - * Sets list of {@link GraphNodeOptions} that represents {@link io.knotx.fragments.engine.Task} - * that will be executed in parallel. + * The array/list of subgraphs/subtasks that can be executed in parallel. * - * @param subtasks list of {@link GraphNodeOptions} + * @param subtasks list of subgraphs * @return reference to this, so the API can be used fluently */ - public SubtasksNodeConfigOptions setSubtasks(List subtasks) { + public SubtasksNodeConfig setSubtasks(List subtasks) { this.subtasks = subtasks; return this; } @@ -75,7 +66,7 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) { return false; } - SubtasksNodeConfigOptions that = (SubtasksNodeConfigOptions) o; + SubtasksNodeConfig that = (SubtasksNodeConfig) o; return Objects.equals(subtasks, that.subtasks); } diff --git a/handler/core/src/main/java/io/knotx/fragments/task/factory/node/subtasks/SubtasksNodeFactory.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/subtasks/SubtasksNodeFactory.java new file mode 100644 index 00000000..8bb4ca98 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/subtasks/SubtasksNodeFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory.node.subtasks; + +import static io.knotx.fragments.handler.api.domain.FragmentResult.ERROR_TRANSITION; +import static io.knotx.fragments.handler.api.domain.FragmentResult.SUCCESS_TRANSITION; + +import io.knotx.fragments.engine.graph.CompositeNode; +import io.knotx.fragments.engine.graph.Node; +import io.knotx.fragments.task.factory.NodeProvider; +import io.knotx.fragments.task.factory.node.NodeFactory; +import io.knotx.fragments.task.factory.GraphNodeOptions; +import io.vertx.core.json.JsonObject; +import io.vertx.reactivex.core.Vertx; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class SubtasksNodeFactory implements NodeFactory { + + public static final String NAME = "subtasks"; + public static final String COMPOSITE_NODE_ID = "composite"; + + @Override + public String getName() { + return NAME; + } + + @Override + public SubtasksNodeFactory configure(JsonObject config, Vertx vertx) { + // empty + return this; + } + + @Override + public Node initNode(GraphNodeOptions nodeOptions, Map edges, + NodeProvider nodeProvider) { + SubtasksNodeConfig config = new SubtasksNodeConfig(nodeOptions.getNode().getConfig()); + List nodes = config.getSubtasks().stream() + .map(nodeProvider::initNode) + .collect(Collectors.toList()); + return new CompositeNode(getNodeId(), nodes, edges.get(SUCCESS_TRANSITION), + edges.get(ERROR_TRANSITION)); + } + + private String getNodeId() { + // TODO https://github.com/Knotx/knotx-fragments/issues/54 + return COMPOSITE_NODE_ID; + } +} diff --git a/handler/core/src/main/resources/META-INF/services/io.knotx.fragments.task.TaskProviderFactory b/handler/core/src/main/resources/META-INF/services/io.knotx.fragments.task.TaskFactory similarity index 91% rename from handler/core/src/main/resources/META-INF/services/io.knotx.fragments.task.TaskProviderFactory rename to handler/core/src/main/resources/META-INF/services/io.knotx.fragments.task.TaskFactory index 5afade95..82553982 100644 --- a/handler/core/src/main/resources/META-INF/services/io.knotx.fragments.task.TaskProviderFactory +++ b/handler/core/src/main/resources/META-INF/services/io.knotx.fragments.task.TaskFactory @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -io.knotx.fragments.task.ConfigurationTaskProviderFactory \ No newline at end of file +io.knotx.fragments.task.factory.DefaultTaskFactory \ No newline at end of file diff --git a/handler/core/src/main/resources/META-INF/services/io.knotx.fragments.task.factory.node.NodeFactory b/handler/core/src/main/resources/META-INF/services/io.knotx.fragments.task.factory.node.NodeFactory new file mode 100644 index 00000000..896198e3 --- /dev/null +++ b/handler/core/src/main/resources/META-INF/services/io.knotx.fragments.task.factory.node.NodeFactory @@ -0,0 +1,16 @@ +# Copyright (C) 2019 Knot.x Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +io.knotx.fragments.task.factory.node.action.ActionNodeFactory +io.knotx.fragments.task.factory.node.subtasks.SubtasksNodeFactory \ No newline at end of file diff --git a/handler/core/src/test/java/io/knotx/fragments/HoconLoader.java b/handler/core/src/test/java/io/knotx/fragments/HoconLoader.java new file mode 100644 index 00000000..bc6a418f --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/HoconLoader.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments; + +import io.vertx.config.ConfigRetriever; +import io.vertx.config.ConfigRetrieverOptions; +import io.vertx.config.ConfigStoreOptions; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxTestContext; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.junit.jupiter.api.Assertions; + +public class HoconLoader { + + public static void verify(String fileName, Consumer assertions, + io.vertx.reactivex.core.Vertx vertx) + throws Throwable { + verify(fileName, assertions, new VertxTestContext(), vertx.getDelegate()); + } + + public static void verify(String fileName, Consumer assertions, + VertxTestContext testContext, io.vertx.reactivex.core.Vertx vertx) + throws Throwable { + verify(fileName, assertions, testContext, vertx.getDelegate()); + } + + public static void verify(String fileName, Consumer assertions, Vertx vertx) + throws Throwable { + verify(fileName, assertions, new VertxTestContext(), vertx); + } + + public static void verify(String fileName, Consumer assertions, + VertxTestContext testContext, Vertx vertx) + throws Throwable { + Handler> configHandler = testContext + .succeeding(config -> testContext.verify(() -> { + assertions.accept(config); + testContext.completeNow(); + })); + fromHOCON(fileName, vertx, configHandler); + + Assertions.assertTrue(testContext.awaitCompletion(5, TimeUnit.SECONDS)); + if (testContext.failed()) { + throw testContext.causeOfFailure(); + } + } + + private static void fromHOCON(String fileName, Vertx vertx, + Handler> configHandler) { + ConfigRetrieverOptions options = new ConfigRetrieverOptions(); + options.addStore(new ConfigStoreOptions() + .setType("file") + .setFormat("hocon") + .setConfig(new JsonObject().put("path", fileName))); + + ConfigRetriever retriever = ConfigRetriever.create(vertx, options); + retriever.getConfig(configHandler); + } + +} diff --git a/handler/core/src/test/java/io/knotx/fragments/handler/FragmentsHandlerTest.java b/handler/core/src/test/java/io/knotx/fragments/handler/FragmentsHandlerTest.java index 124912e8..2857f245 100644 --- a/handler/core/src/test/java/io/knotx/fragments/handler/FragmentsHandlerTest.java +++ b/handler/core/src/test/java/io/knotx/fragments/handler/FragmentsHandlerTest.java @@ -16,24 +16,29 @@ package io.knotx.fragments.handler; import static com.google.common.collect.Lists.newArrayList; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; +import io.knotx.fragments.HoconLoader; import io.knotx.fragments.api.Fragment; -import io.knotx.fragments.handler.options.FragmentsHandlerOptions; -import io.knotx.junit5.util.FileReader; +import io.knotx.fragments.engine.FragmentEvent; +import io.knotx.fragments.engine.FragmentEvent.Status; +import io.knotx.fragments.handler.exception.TaskFactoryNameNotDefinedException; +import io.knotx.fragments.handler.exception.TaskFactoryNotFoundException; +import io.knotx.fragments.task.factory.DefaultTaskFactoryConfig; import io.knotx.server.api.context.ClientRequest; import io.knotx.server.api.context.RequestContext; import io.knotx.server.api.context.RequestEvent; +import io.reactivex.Single; import io.vertx.core.json.JsonObject; import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; import io.vertx.reactivex.core.Vertx; import io.vertx.reactivex.ext.web.RoutingContext; -import java.io.IOException; -import java.util.concurrent.TimeUnit; +import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,61 +52,156 @@ @MockitoSettings(strictness = Strictness.LENIENT) class FragmentsHandlerTest { + private static final String CUSTOM_TASK_NAME_KEY = "task"; + private static final String EMPTY_BODY = ""; + + @Test + @DisplayName("Expect continuing processing next handler when no fragment is failed.") + void shouldSuccess(Vertx vertx, VertxTestContext testContext) + throws Throwable { + HoconLoader.verify("handler/singleTaskFactoryWithSuccessTask.conf", config -> { + //given + RoutingContext routingContext = mockRoutingContext("success-task"); + FragmentsHandler underTest = new FragmentsHandler(vertx, config); + + //when + underTest.handle(routingContext); + + //then + doAnswer(invocation -> { + testContext.completeNow(); + return null; + }) + .when(routingContext) + .next(); + }, testContext, vertx); + } + @Test @DisplayName("Expect fail with Status Code 500 when any fragment is failed.") void shouldFail(Vertx vertx, VertxTestContext testContext) throws Throwable { - //given - RoutingContext routingContext = mockRoutingContext("failing-task"); - - FragmentsHandler underTest = new FragmentsHandler(vertx, - from("tasks/fragment-handler/failingAction.json")); - - //when - underTest.handle(routingContext); - - //then - doAnswer(invocation -> { - testContext.completeNow(); - return null; - }) - .when(routingContext) - .fail(500); - - assertTrue(testContext.awaitCompletion(5, TimeUnit.SECONDS)); - - if (testContext.failed()) { - throw testContext.causeOfFailure(); - } - + HoconLoader.verify("handler/singleTaskFactoryWithFailingTask.conf", config -> { + //given + RoutingContext routingContext = mockRoutingContext("failing-task"); + FragmentsHandler underTest = new FragmentsHandler(vertx, config); + + //when + underTest.handle(routingContext); + + //then + doAnswer(invocation -> { + testContext.completeNow(); + return null; + }) + .when(routingContext) + .fail(500); + }, testContext, vertx); } @Test - @DisplayName("Expect continuing processing next handler when no fragment is failed.") - void shouldSuccess(Vertx vertx, VertxTestContext testContext) + @DisplayName("Expect processed fragment when factory accepts fragment.") + void singleFactory(Vertx vertx, VertxTestContext testContext) throws Throwable { - //given - RoutingContext routingContext = mockRoutingContext("success-task"); - - FragmentsHandler underTest = new FragmentsHandler(vertx, - from("tasks/fragment-handler/successAction.json")); + HoconLoader.verify("handler/singleTaskFactoryWithSuccessTask.conf", config -> { + //given + FragmentsHandler underTest = new FragmentsHandler(vertx, config); + Fragment fragment = new Fragment("type", + new JsonObject().put(DefaultTaskFactoryConfig.DEFAULT_TASK_NAME_KEY, "success-task"), EMPTY_BODY); + String expectedBody = "success"; + + //when + Single> rxDoHandle = underTest + .doHandle(Collections.singletonList(fragment), new ClientRequest()); + + rxDoHandle.subscribe( + result -> testContext.verify(() -> { + // then + FragmentEvent fragmentEvent = result.get(0); + assertEquals(Status.SUCCESS, fragmentEvent.getStatus()); + assertEquals(expectedBody, fragmentEvent.getFragment().getBody()); + testContext.completeNow(); + }), + testContext::failNow + ); + }, testContext, vertx); + } - //when - underTest.handle(routingContext); + @Test + @DisplayName("Expect unprocessed fragment when all factories do not accept fragment.") + void singleFactoryNotAcceptingFragment(Vertx vertx, VertxTestContext testContext) + throws Throwable { + HoconLoader.verify("handler/singleTaskFactoryWithSuccessTask.conf", config -> { + //given + FragmentsHandler underTest = new FragmentsHandler(vertx, config); + Fragment fragment = new Fragment("type", new JsonObject(), EMPTY_BODY); + + //when + Single> rxDoHandle = underTest + .doHandle(Collections.singletonList(fragment), new ClientRequest()); + + rxDoHandle.subscribe( + result -> testContext.verify(() -> { + // then + assertEquals(Status.UNPROCESSED, result.get(0).getStatus()); + testContext.completeNow(); + }), + testContext::failNow + ); + }, testContext, vertx); + } - //then - doAnswer(invocation -> { - testContext.completeNow(); - return null; - }) - .when(routingContext) - .next(); + @Test + @DisplayName("Expect processed fragment when second factory accepts fragment.") + void twoFactoriesWithTheSameName(Vertx vertx, VertxTestContext testContext) + throws Throwable { + HoconLoader.verify("handler/manyTaskFactoriesWithTheSameName.conf", config -> { + //given + FragmentsHandler underTest = new FragmentsHandler(vertx, config); + Fragment fragment = new Fragment("type", + new JsonObject().put(CUSTOM_TASK_NAME_KEY, "success-task"), EMPTY_BODY); + String expectedBody = "custom"; + + //when + Single> rxDoHandle = underTest + .doHandle(Collections.singletonList(fragment), new ClientRequest()); + + rxDoHandle.subscribe( + result -> testContext.verify(() -> { + assertEquals(expectedBody, result.get(0).getFragment().getBody()); + testContext.completeNow(); + }), + testContext::failNow + ); + }, testContext, vertx); + } - assertTrue(testContext.awaitCompletion(5, TimeUnit.SECONDS)); + @Test + @DisplayName("Expect exception when task factory name is not defined") + void taskFactoryNameNotDefined(Vertx vertx, VertxTestContext testContext) + throws Throwable { + HoconLoader.verify("handler/taskFactoryNameNotDefined.conf", config -> { + //given + try { + new FragmentsHandler(vertx, config); + } catch (TaskFactoryNameNotDefinedException e) { + testContext.completed(); + } + }, testContext, vertx); + } - if (testContext.failed()) { - throw testContext.causeOfFailure(); - } + @Test + @DisplayName("Expect exception when task factory name is not found") + void taskFactoryNotFound(Vertx vertx, VertxTestContext testContext) + throws Throwable { + HoconLoader.verify("handler/taskFactoryNotFound.conf", config -> { + //given + try { + new FragmentsHandler(vertx, config); + } catch (TaskFactoryNotFoundException e) { + testContext.completed(); + } + }, testContext, vertx); } private RoutingContext mockRoutingContext(String task) { @@ -118,10 +218,6 @@ private RoutingContext mockRoutingContext(String task) { private Fragment fragment(String task) { return new Fragment("type", - new JsonObject().put(FragmentsHandlerOptions.DEFAULT_TASK_KEY, task), ""); - } - - private JsonObject from(String fileName) throws IOException { - return new JsonObject(FileReader.readText(fileName)); + new JsonObject().put(DefaultTaskFactoryConfig.DEFAULT_TASK_NAME_KEY, task), ""); } } diff --git a/handler/core/src/test/java/io/knotx/fragments/handler/TestAction.java b/handler/core/src/test/java/io/knotx/fragments/handler/TestAction.java index d8a927af..48635261 100644 --- a/handler/core/src/test/java/io/knotx/fragments/handler/TestAction.java +++ b/handler/core/src/test/java/io/knotx/fragments/handler/TestAction.java @@ -39,7 +39,7 @@ public Action create(String alias, JsonObject config, Vertx vertx, Action doActi return (fragmentContext, resultHandler) -> { Fragment fragment = fragmentContext.getFragment(); - fragment.setBody("body"); + fragment.setBody(config.getString("body", "any")); Future resultFuture = succeededFuture(new FragmentResult(fragment, transition)); resultFuture.setHandler(resultHandler); diff --git a/handler/core/src/test/java/io/knotx/fragments/task/ConfigurationTaskProviderTest.java b/handler/core/src/test/java/io/knotx/fragments/task/ConfigurationTaskProviderTest.java deleted file mode 100644 index acafd19b..00000000 --- a/handler/core/src/test/java/io/knotx/fragments/task/ConfigurationTaskProviderTest.java +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright (C) 2019 Knot.x Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * The code comes from https://github.com/tomaszmichalak/vertx-rx-map-reduce. - */ -package io.knotx.fragments.task; - -import static io.knotx.fragments.handler.api.domain.FragmentResult.ERROR_TRANSITION; -import static io.knotx.fragments.handler.api.domain.FragmentResult.SUCCESS_TRANSITION; -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 static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - -import io.knotx.fragments.api.Fragment; -import io.knotx.fragments.engine.FragmentEvent; -import io.knotx.fragments.engine.FragmentEventContext; -import io.knotx.fragments.engine.Task; -import io.knotx.fragments.engine.graph.SingleNode; -import io.knotx.fragments.engine.graph.CompositeNode; -import io.knotx.fragments.engine.graph.Node; -import io.knotx.fragments.handler.action.ActionProvider; -import io.knotx.fragments.handler.api.Action; -import io.knotx.fragments.handler.options.FragmentsHandlerOptions; -import io.knotx.fragments.task.exception.GraphConfigurationException; -import io.knotx.fragments.task.options.GraphNodeOptions; -import io.knotx.server.api.context.ClientRequest; -import io.vertx.core.json.JsonObject; -import io.vertx.junit5.VertxExtension; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; - -@ExtendWith(VertxExtension.class) -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class ConfigurationTaskProviderTest { - - private static final Map NO_TRANSITIONS = Collections.emptyMap(); - private static final String TASK_NAME = "task"; - private static final String COMPOSITE_NODE_ID = "composite"; - private static final FragmentEventContext SAMPLE_FRAGMENT_EVENT = - new FragmentEventContext(new FragmentEvent(new Fragment("type", - new JsonObject().put(FragmentsHandlerOptions.DEFAULT_TASK_KEY, TASK_NAME), "body")), - new ClientRequest()); - - private static final String MY_TASK_KEY = "myTaskKey"; - - private static final FragmentEventContext SAMPLE_FRAGMENT_EVENT_WITH_CUSTOM_TASK_KEY = - new FragmentEventContext(new FragmentEvent(new Fragment("type", - new JsonObject().put(MY_TASK_KEY, TASK_NAME), "body")), - new ClientRequest()); - - @Mock - private ActionProvider actionProvider; - - @Mock - private Action actionMock; - - @Test - @DisplayName("Expect graph when custom task key is defined.") - void expectGraphWhenCustomTaskKey() { - // given - when(actionProvider.get(eq("simpleAction"))).thenReturn(Optional.of(actionMock)); - - GraphNodeOptions graph = new GraphNodeOptions("simpleAction", NO_TRANSITIONS); - - // when - Task task = new ConfigurationTaskProvider(actionProvider) - .newInstance(new Configuration(TASK_NAME, graph), SAMPLE_FRAGMENT_EVENT_WITH_CUSTOM_TASK_KEY); - - // then - assertEquals(TASK_NAME, task.getName()); - } - - @Test - @DisplayName("Expect exception when action not defined.") - void expectExceptionWhenActionNotConfigured() { - // given - when(actionProvider.get(eq("simpleAction"))).thenReturn(Optional.empty()); - - GraphNodeOptions graph = new GraphNodeOptions("simpleAction", NO_TRANSITIONS); - - // when, then - Assertions.assertThrows(GraphConfigurationException.class, - () -> getTask(graph)); - } - - @Test - @DisplayName("Expect graph with single action node without transitions.") - void expectSingleActionNodeGraph() { - // given - when(actionProvider.get(eq("simpleAction"))).thenReturn(Optional.of(actionMock)); - - GraphNodeOptions graph = new GraphNodeOptions("simpleAction", NO_TRANSITIONS); - - // when - Task task = getTask(graph); - - // then - assertEquals(TASK_NAME, task.getName()); - assertTrue(task.getRootNode().isPresent()); - Node rootNode = task.getRootNode().get(); - assertTrue(rootNode instanceof SingleNode); - assertEquals("simpleAction", rootNode.getId()); - assertFalse(rootNode.next(SUCCESS_TRANSITION).isPresent()); - } - - @Test - @DisplayName("Expect graph of two action nodes with transition between.") - void expectActionNodesGraphWithTransition() { - // given - when(actionProvider.get("actionA")).thenReturn(Optional.of(actionMock)); - when(actionProvider.get("actionB")).thenReturn(Optional.of(actionMock)); - - GraphNodeOptions graph = new GraphNodeOptions("actionA", Collections - .singletonMap("customTransition", - new GraphNodeOptions("actionB", NO_TRANSITIONS))); - - // when - Task task = getTask(graph); - - // then - assertEquals(TASK_NAME, task.getName()); - - assertTrue(task.getRootNode().isPresent()); - Node rootNode = task.getRootNode().get(); - assertTrue(rootNode instanceof SingleNode); - assertEquals("actionA", rootNode.getId()); - Optional customNode = rootNode.next("customTransition"); - assertTrue(customNode.isPresent()); - assertTrue(customNode.get() instanceof SingleNode); - SingleNode customSingleNode = (SingleNode) customNode.get(); - assertEquals("actionB", customSingleNode.getId()); - } - - @Test - @DisplayName("Expect graph with single composite node without transitions.") - void expectSingleCompositeNodeGraphWithNoEdges() { - // given - when(actionProvider.get(eq("simpleAction"))).thenReturn(Optional.of(actionMock)); - - GraphNodeOptions graph = new GraphNodeOptions( - subTasks(new GraphNodeOptions("simpleAction", NO_TRANSITIONS)), - NO_TRANSITIONS - ); - - // when - Task task = getTask(graph); - - // then - assertEquals(TASK_NAME, task.getName()); - assertTrue(task.getRootNode().isPresent()); - Node rootNode = task.getRootNode().get(); - assertTrue(rootNode instanceof CompositeNode); - assertEquals(COMPOSITE_NODE_ID, rootNode.getId()); - assertFalse(rootNode.next(SUCCESS_TRANSITION).isPresent()); - assertFalse(rootNode.next(ERROR_TRANSITION).isPresent()); - - CompositeNode compositeRootNode = (CompositeNode) rootNode; - assertEquals(1, compositeRootNode.getNodes().size()); - Node node = compositeRootNode.getNodes().get(0); - assertTrue(node instanceof SingleNode); - assertEquals("simpleAction", node.getId()); - } - - - @Test - @DisplayName("Expect graph with composite node and success transition to action node.") - void expectCompositeNodeWithSingleNodeOnSuccessGraph() { - // given - when(actionProvider.get(eq("simpleAction"))).thenReturn(Optional.of(actionMock)); - when(actionProvider.get(eq("lastAction"))).thenReturn(Optional.of(actionMock)); - - GraphNodeOptions graph = new GraphNodeOptions( - subTasks(new GraphNodeOptions("simpleAction", NO_TRANSITIONS)), - Collections - .singletonMap(SUCCESS_TRANSITION, new GraphNodeOptions("lastAction", NO_TRANSITIONS)) - ); - - // when - Task task = getTask(graph); - - // then - assertEquals(TASK_NAME, task.getName()); - assertTrue(task.getRootNode().isPresent()); - Node rootNode = task.getRootNode().get(); - assertTrue(rootNode instanceof CompositeNode); - assertEquals(COMPOSITE_NODE_ID, rootNode.getId()); - Optional onSuccess = rootNode.next(SUCCESS_TRANSITION); - assertTrue(onSuccess.isPresent()); - Node onSuccessNode = onSuccess.get(); - assertTrue(onSuccessNode instanceof SingleNode); - assertEquals("lastAction", onSuccessNode.getId()); - } - - @Test - @DisplayName("Expect graph with composite node and error transition to action node.") - void expectCompositeNodeWithSingleNodeOnErrorGraph() { - // given - when(actionProvider.get(eq("simpleAction"))).thenReturn(Optional.of(actionMock)); - when(actionProvider.get(eq("fallbackAction"))).thenReturn(Optional.of(actionMock)); - - GraphNodeOptions graph = new GraphNodeOptions( - subTasks(new GraphNodeOptions("simpleAction", NO_TRANSITIONS)), - Collections.singletonMap(ERROR_TRANSITION, - new GraphNodeOptions("fallbackAction", NO_TRANSITIONS)) - ); - - // when - Task task = getTask(graph); - - // then - assertEquals(TASK_NAME, task.getName()); - assertTrue(task.getRootNode().isPresent()); - Node rootNode = task.getRootNode().get(); - assertTrue(rootNode instanceof CompositeNode); - assertEquals(COMPOSITE_NODE_ID, rootNode.getId()); - Optional onError = rootNode.next(ERROR_TRANSITION); - assertTrue(onError.isPresent()); - Node onErrorNode = onError.get(); - assertTrue(onErrorNode instanceof SingleNode); - assertEquals("fallbackAction", onErrorNode.getId()); - } - - @Test - void expectCompositeNodeAcceptsOnlySuccessAndErrorTransitions() { - // given - when(actionProvider.get(eq("simpleAction"))).thenReturn(Optional.of(actionMock)); - when(actionProvider.get(eq("lastAction"))).thenReturn(Optional.of(actionMock)); - - GraphNodeOptions graph = new GraphNodeOptions( - subTasks(new GraphNodeOptions("simpleAction", NO_TRANSITIONS)), - Collections - .singletonMap("customTransition", new GraphNodeOptions("lastAction", NO_TRANSITIONS)) - ); - - // when - Task task = getTask(graph); - - // then - assertTrue(task.getRootNode().isPresent()); - Node rootNode = task.getRootNode().get(); - assertTrue(rootNode instanceof CompositeNode); - assertFalse(rootNode.next(SUCCESS_TRANSITION).isPresent()); - assertFalse(rootNode.next(ERROR_TRANSITION).isPresent()); - assertFalse(rootNode.next("customTransition").isPresent()); - } - - @Test - @DisplayName("Expect graph with nested composite nodes") - void expectNestedCompositeNodesGraph() { - // given - when(actionProvider.get(eq("simpleAction"))).thenReturn(Optional.of(actionMock)); - - GraphNodeOptions graph = new GraphNodeOptions( - subTasks( - new GraphNodeOptions(subTasks(new GraphNodeOptions("simpleAction", NO_TRANSITIONS)), - NO_TRANSITIONS)), - NO_TRANSITIONS - ); - - // when - Task task = getTask(graph); - - // then - assertEquals(TASK_NAME, task.getName()); - assertTrue(task.getRootNode().isPresent()); - Node rootNode = task.getRootNode().get(); - assertTrue(rootNode instanceof CompositeNode); - assertEquals(COMPOSITE_NODE_ID, rootNode.getId()); - - CompositeNode compositeRootNode = (CompositeNode) rootNode; - assertEquals(1, compositeRootNode.getNodes().size()); - Node childNode = compositeRootNode.getNodes().get(0); - assertEquals(COMPOSITE_NODE_ID, childNode.getId()); - assertTrue(childNode instanceof CompositeNode); - CompositeNode compositeChildNode = (CompositeNode) childNode; - - assertEquals(1, compositeChildNode.getNodes().size()); - Node node = compositeChildNode.getNodes().get(0); - assertTrue(node instanceof SingleNode); - assertEquals("simpleAction", node.getId()); - } - - private Task getTask(GraphNodeOptions graph) { - return new ConfigurationTaskProvider(actionProvider) - .newInstance(new Configuration(TASK_NAME, graph), SAMPLE_FRAGMENT_EVENT); - } - - private List subTasks(GraphNodeOptions... nodes) { - return Arrays.asList(nodes); - } -} diff --git a/handler/core/src/test/java/io/knotx/fragments/handler/action/ActionProviderTest.java b/handler/core/src/test/java/io/knotx/fragments/task/factory/ActionProviderTest.java similarity index 66% rename from handler/core/src/test/java/io/knotx/fragments/handler/action/ActionProviderTest.java rename to handler/core/src/test/java/io/knotx/fragments/task/factory/ActionProviderTest.java index 6dd54a4d..622a0fb9 100644 --- a/handler/core/src/test/java/io/knotx/fragments/handler/action/ActionProviderTest.java +++ b/handler/core/src/test/java/io/knotx/fragments/task/factory/ActionProviderTest.java @@ -12,10 +12,8 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * - * The code comes from https://github.com/tomaszmichalak/vertx-rx-map-reduce. */ -package io.knotx.fragments.handler.action; +package io.knotx.fragments.task.factory; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; @@ -23,20 +21,22 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.google.common.collect.ImmutableMap; - -import io.knotx.fragments.handler.api.domain.FragmentContext; -import io.knotx.fragments.handler.api.domain.FragmentResult; -import io.knotx.fragments.handler.api.Cacheable; import io.knotx.fragments.handler.api.Action; import io.knotx.fragments.handler.api.ActionFactory; +import io.knotx.fragments.handler.api.Cacheable; +import io.knotx.fragments.handler.api.domain.FragmentContext; +import io.knotx.fragments.handler.api.domain.FragmentResult; +import io.knotx.fragments.task.factory.node.action.ActionProvider; import io.vertx.core.AsyncResult; import io.vertx.core.Handler; -import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; import io.vertx.junit5.VertxExtension; +import io.vertx.reactivex.core.Vertx; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -45,7 +45,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(VertxExtension.class) @@ -61,8 +60,8 @@ class ActionProviderTest { @DisplayName("Expect no action when empty or null action alias defined.") void getWithNoAction(Vertx vertx) { // given - ActionProvider tested = new ActionProvider(Collections.emptyMap(), - Collections::emptyListIterator, "error", vertx); + ActionProvider tested = new ActionProvider(Collections::emptyListIterator, + Collections.emptyMap(), vertx); // when Optional operation = tested.get(null); @@ -75,8 +74,8 @@ void getWithNoAction(Vertx vertx) { @DisplayName("Expect no action when no action alias defined in configuration.") void getWithNoEntries(Vertx vertx) { // given - ActionProvider tested = new ActionProvider(Collections.emptyMap(), - Collections::emptyListIterator, "error", vertx); + ActionProvider tested = new ActionProvider(Collections::emptyListIterator, + Collections.emptyMap(), vertx); // when Optional operation = tested.get("any"); @@ -89,12 +88,11 @@ void getWithNoEntries(Vertx vertx) { @DisplayName("Expect no action when no factory found.") void getWithNoFactory(Vertx vertx) { // given - Map proxies = Collections - .singletonMap(PROXY_ALIAS, new ActionOptions("eb", new JsonObject(), null)); + Map proxies = Collections + .singletonMap(PROXY_ALIAS, new ActionFactoryOptions("eb", new JsonObject(), null)); - ActionProvider tested = new ActionProvider(proxies, - Collections::emptyListIterator, "error", - vertx); + ActionProvider tested = new ActionProvider(Collections::emptyListIterator, + proxies, vertx); // when Optional operation = tested.get(PROXY_ALIAS); @@ -107,14 +105,14 @@ void getWithNoFactory(Vertx vertx) { @DisplayName("Expect action when action alias defined and factory found.") void getOperation(Vertx vertx) { // given - Map proxies = Collections + Map proxies = Collections .singletonMap(PROXY_ALIAS, - new ActionOptions(PROXY_FACTORY_NAME, new JsonObject(), null)); + new ActionFactoryOptions(PROXY_FACTORY_NAME, new JsonObject(), null)); List factories = Collections .singletonList(new TestCacheableOperationFactory()); - ActionProvider tested = new ActionProvider(proxies, - factories::iterator, "error", vertx); + ActionProvider tested = new ActionProvider(factories::iterator, proxies, + vertx); // when Optional operation = tested.get(PROXY_ALIAS); @@ -123,18 +121,16 @@ void getOperation(Vertx vertx) { assertTrue(operation.isPresent()); } + @Test @DisplayName("Expect action when action config not defined.") - void getOperationWithNoActionConfig(Vertx vertx) { + void expectActionWithNoActionConfig(Vertx vertx) { // given - Map proxies = Collections - .singletonMap(PROXY_ALIAS, - new ActionOptions(PROXY_FACTORY_NAME, null, null)); - List factories = Collections - .singletonList(new TestCacheableOperationFactory()); + Map proxies = Collections + .singletonMap(PROXY_ALIAS, new ActionFactoryOptions(PROXY_FACTORY_NAME)); + List factories = Collections.singletonList(new TestCacheableOperationFactory()); - ActionProvider tested = new ActionProvider(proxies, - factories::iterator, "error", vertx); + ActionProvider tested = new ActionProvider(factories::iterator, proxies, vertx); // when Optional operation = tested.get(PROXY_ALIAS); @@ -147,13 +143,13 @@ void getOperationWithNoActionConfig(Vertx vertx) { @DisplayName("Expect new action every time we call non cacheable factory.") void getNewAction(Vertx vertx) { // given - Map proxies = Collections + Map proxies = Collections .singletonMap(PROXY_ALIAS, - new ActionOptions(PROXY_FACTORY_NAME, new JsonObject(), null)); + new ActionFactoryOptions(PROXY_FACTORY_NAME, new JsonObject(), null)); List factories = Collections .singletonList(new TestOperationFactory()); - ActionProvider tested = new ActionProvider(proxies, factories::iterator, "error", vertx); + ActionProvider tested = new ActionProvider(factories::iterator, proxies, vertx); // when Optional firstOperation = tested.get(PROXY_ALIAS); @@ -169,14 +165,14 @@ void getNewAction(Vertx vertx) { @DisplayName("Expect the same action every time we call cacheable factory.") void getCachedOperation(Vertx vertx) { // given - Map proxies = Collections + Map proxies = Collections .singletonMap(PROXY_ALIAS, - new ActionOptions(PROXY_FACTORY_NAME, new JsonObject(), null)); + new ActionFactoryOptions(PROXY_FACTORY_NAME, new JsonObject(), null)); List factories = Collections .singletonList(new TestCacheableOperationFactory()); - ActionProvider tested = new ActionProvider(proxies, - factories::iterator, "error", vertx); + ActionProvider tested = new ActionProvider(factories::iterator, proxies, + vertx); // when Optional firstOperation = tested.get(PROXY_ALIAS); @@ -192,41 +188,43 @@ void getCachedOperation(Vertx vertx) { @DisplayName("Expect not null action defined as doAction while creating action.") void getComplexOperation(Vertx vertx) { // given - Action expectedOperation = Mockito.mock(Action.class); - Action expectedOperationSecond = Mockito.mock(Action.class); + Action expectedOperation = mock(Action.class); + Action expectedOperationSecond = mock(Action.class); - ActionFactory proxyFactory = Mockito.mock(ActionFactory.class); + ActionFactory proxyFactory = mock(ActionFactory.class); when(proxyFactory.getName()).thenReturn(PROXY_FACTORY_NAME); when(proxyFactory - .create(eq(PROXY_ALIAS), any(), eq(vertx), eq(expectedOperationSecond))) + .create(eq(PROXY_ALIAS), any(), eq(vertx.getDelegate()), eq(expectedOperationSecond))) .thenReturn(expectedOperation); - ActionFactory proxyFactorySecond = Mockito.mock(ActionFactory.class); + ActionFactory proxyFactorySecond = mock(ActionFactory.class); when(proxyFactorySecond.getName()).thenReturn(PROXY_FACTORY_NAME_SECOND); - when(proxyFactorySecond.create(eq(PROXY_ALIAS_SECOND), any(), eq(vertx), eq(null))) + when( + proxyFactorySecond.create(eq(PROXY_ALIAS_SECOND), any(), eq(vertx.getDelegate()), eq(null))) .thenReturn(expectedOperationSecond); - Map proxies = ImmutableMap.of( + Map proxies = ImmutableMap.of( PROXY_ALIAS, - new ActionOptions(PROXY_FACTORY_NAME, new JsonObject(), PROXY_ALIAS_SECOND), + new ActionFactoryOptions(PROXY_FACTORY_NAME, new JsonObject(), PROXY_ALIAS_SECOND), PROXY_ALIAS_SECOND, - new ActionOptions(PROXY_FACTORY_NAME_SECOND, new JsonObject()) + new ActionFactoryOptions(PROXY_FACTORY_NAME_SECOND, new JsonObject()) ); List factories = Arrays.asList(proxyFactory, proxyFactorySecond); - ActionProvider tested = new ActionProvider(proxies, factories::iterator, "error", vertx); + ActionProvider tested = new ActionProvider(factories::iterator, proxies, + vertx); // when tested.get(PROXY_ALIAS); // then - Mockito.verify(proxyFactorySecond) - .create(eq(PROXY_ALIAS_SECOND), any(), eq(vertx), eq(null)); - Mockito.verify(proxyFactory) - .create(eq(PROXY_ALIAS), any(), eq(vertx), eq(expectedOperationSecond)); + verify(proxyFactorySecond) + .create(eq(PROXY_ALIAS_SECOND), any(), eq(vertx.getDelegate()), eq(null)); + verify(proxyFactory) + .create(eq(PROXY_ALIAS), any(), eq(vertx.getDelegate()), eq(expectedOperationSecond)); } - class TestOperationFactory implements ActionFactory { + static class TestOperationFactory implements ActionFactory { @Override public String getName() { @@ -234,7 +232,7 @@ public String getName() { } @Override - public Action create(String alias, JsonObject config, Vertx vertx, + public Action create(String alias, JsonObject config, io.vertx.core.Vertx vertx, Action doAction) { // do not change to lambda expression as it can be optimised by compiler return new Action() { @@ -248,7 +246,7 @@ public void apply(FragmentContext fragmentContext, } @Cacheable - class TestCacheableOperationFactory implements ActionFactory { + static class TestCacheableOperationFactory implements ActionFactory { @Override public String getName() { @@ -256,7 +254,7 @@ public String getName() { } @Override - public Action create(String alias, JsonObject config, Vertx vertx, + public Action create(String alias, JsonObject config, io.vertx.core.Vertx vertx, Action doAction) { // do not change to lambda expression as it can be optimised by compiler return new Action() { diff --git a/handler/core/src/test/java/io/knotx/fragments/task/factory/DefaultTaskFactoryConfigTest.java b/handler/core/src/test/java/io/knotx/fragments/task/factory/DefaultTaskFactoryConfigTest.java new file mode 100644 index 00000000..52571d94 --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/task/factory/DefaultTaskFactoryConfigTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory; + +import static io.knotx.fragments.HoconLoader.verify; + +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxExtension; +import java.util.function.Consumer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(VertxExtension.class) +class DefaultTaskFactoryConfigTest { + + @Test + @DisplayName("Expect all node factories do not contain log level when global is not configured") + void expectNoLogLevel(Vertx vertx) throws Throwable { + verify("task/factory/taskFactoryWithNoGlobalLogLevel.conf", validateNoGlobalNodeLog(), vertx); + } + + @Test + @DisplayName("Expect all node factories contain log level when global is configured") + void expectLogLevel(Vertx vertx) throws Throwable { + verify("task/factory/taskFactoryWithGlobalLogLevel.conf", validateNodeLog("INFO"), vertx); + } + + @Test + @DisplayName("Expect local node log level is not overridden by global one") + void expectLocalLogLevel(Vertx vertx) throws Throwable { + verify("task/factory/taskFactoryWithLocalLogLevel.conf", validateNodeLog("ERROR"), vertx); + } + + private Consumer validateNoGlobalNodeLog() { + return config -> { + DefaultTaskFactoryConfig factoryConfig = new DefaultTaskFactoryConfig(config); + factoryConfig.getNodeFactories().forEach( + nodeFactoryOptions -> Assertions + .assertNull(new LogLevelConfig(nodeFactoryOptions.getConfig()).getLogLevel()) + ); + }; + } + + private Consumer validateNodeLog(String logLevel) { + return config -> { + DefaultTaskFactoryConfig factoryConfig = new DefaultTaskFactoryConfig(config); + factoryConfig.getNodeFactories().forEach( + nodeFactoryOptions -> Assertions + .assertEquals(logLevel, nodeFactoryOptions.getConfig().getString("logLevel")) + ); + }; + } +} \ No newline at end of file diff --git a/handler/core/src/test/java/io/knotx/fragments/task/factory/DefaultTaskFactoryTest.java b/handler/core/src/test/java/io/knotx/fragments/task/factory/DefaultTaskFactoryTest.java new file mode 100644 index 00000000..05b537b9 --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/task/factory/DefaultTaskFactoryTest.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory; + +import static io.knotx.fragments.handler.api.domain.FragmentResult.SUCCESS_TRANSITION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.knotx.fragments.api.Fragment; +import io.knotx.fragments.engine.FragmentEvent; +import io.knotx.fragments.engine.FragmentEventContext; +import io.knotx.fragments.engine.Task; +import io.knotx.fragments.engine.graph.CompositeNode; +import io.knotx.fragments.engine.graph.Node; +import io.knotx.fragments.engine.graph.SingleNode; +import io.knotx.fragments.task.exception.TaskNotFoundException; +import io.knotx.fragments.task.factory.node.NodeFactoryOptions; +import io.knotx.fragments.task.factory.node.action.ActionNodeFactory; +import io.knotx.fragments.task.factory.node.action.ActionNodeFactoryConfig; +import io.knotx.fragments.task.factory.node.subtasks.SubtasksNodeFactory; +import io.knotx.server.api.context.ClientRequest; +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxExtension; +import io.vertx.reactivex.core.Vertx; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(VertxExtension.class) +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class DefaultTaskFactoryTest { + + private static final String TASK_NAME = "task"; + private static final String COMPOSITE_NODE_ID = "composite"; + private static final Map NO_TRANSITIONS = Collections.emptyMap(); + + private static final FragmentEventContext SAMPLE_FRAGMENT_EVENT = + new FragmentEventContext(new FragmentEvent(new Fragment("type", + new JsonObject().put(DefaultTaskFactoryConfig.DEFAULT_TASK_NAME_KEY, TASK_NAME), "body")), + new ClientRequest()); + + private static final String MY_TASK_KEY = "myTaskKey"; + + private static final FragmentEventContext SAMPLE_FRAGMENT_EVENT_WITH_CUSTOM_TASK_KEY = + new FragmentEventContext(new FragmentEvent(new Fragment("type", + new JsonObject().put(MY_TASK_KEY, TASK_NAME), "body")), + new ClientRequest()); + + @Test + @DisplayName("Expect fragment is not accepted when it does not specify a task.") + void notAcceptFragmentWithoutTask(Vertx vertx) { + // given + FragmentEventContext fragmentWithNoTask = + new FragmentEventContext( + new FragmentEvent( + new Fragment("type", new JsonObject(), "body")), + new ClientRequest() + ); + DefaultTaskFactory tested = new DefaultTaskFactory() + .configure(emptyFactoryConfig().toJson(), vertx); + + // when + boolean accepted = tested.accept(fragmentWithNoTask); + + // then + Assertions.assertFalse(accepted); + } + + @Test + @DisplayName("Expect fragment is not accepted when it specifies a task but it is not configured") + void noAcceptFragmentWhenTaskNotConfigured(Vertx vertx) { + // given + DefaultTaskFactory tested = new DefaultTaskFactory() + .configure(emptyFactoryConfig().toJson(), vertx); + + // when + boolean accepted = tested.accept(SAMPLE_FRAGMENT_EVENT); + + // then + Assertions.assertFalse(accepted); + } + + @Test + @DisplayName("Expect fragment is accepted when it specifies a task and it is configured") + void acceptFragment(Vertx vertx) { + GraphNodeOptions graph = new GraphNodeOptions("A", NO_TRANSITIONS); + + DefaultTaskFactory tested = new DefaultTaskFactory().configure( + createTaskFactoryConfig(graph, new JsonObject()).toJson(), vertx); + + // when + boolean accepted = tested.accept(SAMPLE_FRAGMENT_EVENT); + + // then + Assertions.assertTrue(accepted); + } + + @Test + @DisplayName("Expect fragment is accepted when it specifies a custom task name and it is configured") + void acceptFragmentWhenCustomTaskName(Vertx vertx) { + GraphNodeOptions graph = new GraphNodeOptions("A", NO_TRANSITIONS); + + DefaultTaskFactory tested = new DefaultTaskFactory().configure( + createTaskFactoryConfig(graph, new JsonObject()).setTaskNameKey(MY_TASK_KEY).toJson(), + vertx); + + // when + boolean accepted = tested.accept(SAMPLE_FRAGMENT_EVENT_WITH_CUSTOM_TASK_KEY); + + // then + Assertions.assertTrue(accepted); + } + + @Test + @DisplayName("Expect task not found exception when a fragment does not define a task.") + void newInstanceFailedWhenNoTask(Vertx vertx) { + // given + FragmentEventContext fragmentWithNoTask = + new FragmentEventContext( + new FragmentEvent( + new Fragment("type", new JsonObject(), "body")), + new ClientRequest() + ); + JsonObject actionNodeConfig = createActionNodeConfig("A", SUCCESS_TRANSITION); + GraphNodeOptions graph = new GraphNodeOptions("A", NO_TRANSITIONS); + + // when + Assertions.assertThrows( + TaskNotFoundException.class, + () -> new DefaultTaskFactory() + .configure(createTaskFactoryConfig(graph, actionNodeConfig).toJson(), vertx) + .newInstance(fragmentWithNoTask)); + } + + @Test + @DisplayName("Expect new task instance when task name is defined and configured.") + void newInstance(Vertx vertx) { + // given + JsonObject actionNodeConfig = createActionNodeConfig("A", SUCCESS_TRANSITION); + GraphNodeOptions graph = new GraphNodeOptions("A", NO_TRANSITIONS); + + // when + Task task = new DefaultTaskFactory() + .configure(createTaskFactoryConfig(graph, actionNodeConfig).toJson(), vertx) + .newInstance(SAMPLE_FRAGMENT_EVENT); + + // then + assertEquals(TASK_NAME, task.getName()); + } + + @Test + @DisplayName("Expect always a new task instance.") + void newInstanceAlways(Vertx vertx) { + // given + JsonObject actionNodeConfig = createActionNodeConfig("A", SUCCESS_TRANSITION); + GraphNodeOptions graph = new GraphNodeOptions("A", NO_TRANSITIONS); + + // when + DefaultTaskFactory taskFactory = new DefaultTaskFactory() + .configure(createTaskFactoryConfig(graph, actionNodeConfig).toJson(), vertx); + + // then + assertNotSame( + taskFactory.newInstance(SAMPLE_FRAGMENT_EVENT), + taskFactory.newInstance(SAMPLE_FRAGMENT_EVENT) + ); + } + + @Test + @DisplayName("Expect new task instance when custom task name key is defined.") + void expectGraphWhenCustomTaskKey(Vertx vertx) { + // given + JsonObject actionNodeConfig = createActionNodeConfig("A", SUCCESS_TRANSITION); + GraphNodeOptions graph = new GraphNodeOptions("A", NO_TRANSITIONS); + + // when + Task task = new DefaultTaskFactory() + .configure( + createTaskFactoryConfig(graph, actionNodeConfig).setTaskNameKey(MY_TASK_KEY).toJson(), + vertx) + .newInstance(SAMPLE_FRAGMENT_EVENT_WITH_CUSTOM_TASK_KEY); + + // then + assertEquals(TASK_NAME, task.getName()); + } + + @Test + @DisplayName("Expect graph of two action nodes with transition between.") + void expectNodesWithTransitionBetween(Vertx vertx) { + // given + JsonObject options = createActionNodeConfig("A", SUCCESS_TRANSITION); + merge(options, "B", SUCCESS_TRANSITION); + + GraphNodeOptions graph = new GraphNodeOptions("A", Collections + .singletonMap("customTransition", + new GraphNodeOptions("B", NO_TRANSITIONS))); + + // when + Task task = getTask(graph, options, vertx); + + // then + assertEquals(TASK_NAME, task.getName()); + + assertTrue(task.getRootNode().isPresent()); + Node rootNode = task.getRootNode().get(); + assertTrue(rootNode instanceof SingleNode); + assertEquals("A", rootNode.getId()); + Optional customNode = rootNode.next("customTransition"); + assertTrue(customNode.isPresent()); + assertTrue(customNode.get() instanceof SingleNode); + SingleNode customSingleNode = (SingleNode) customNode.get(); + assertEquals("B", customSingleNode.getId()); + } + + @Test + @DisplayName("Expect graph with nested composite nodes") + void expectNestedCompositeNodesGraph(Vertx vertx) { + // given + JsonObject options = createActionNodeConfig("A", SUCCESS_TRANSITION); + + GraphNodeOptions graph = new GraphNodeOptions( + subTasks( + new GraphNodeOptions(subTasks(new GraphNodeOptions("A", NO_TRANSITIONS)), + NO_TRANSITIONS)), + NO_TRANSITIONS + ); + + // when + Task task = getTask(graph, options, vertx); + + // then + assertEquals(TASK_NAME, task.getName()); + assertTrue(task.getRootNode().isPresent()); + Node rootNode = task.getRootNode().get(); + assertTrue(rootNode instanceof CompositeNode); + assertEquals(COMPOSITE_NODE_ID, rootNode.getId()); + + CompositeNode compositeRootNode = (CompositeNode) rootNode; + assertEquals(1, compositeRootNode.getNodes().size()); + Node childNode = compositeRootNode.getNodes().get(0); + assertEquals(COMPOSITE_NODE_ID, childNode.getId()); + assertTrue(childNode instanceof CompositeNode); + CompositeNode compositeChildNode = (CompositeNode) childNode; + + assertEquals(1, compositeChildNode.getNodes().size()); + Node node = compositeChildNode.getNodes().get(0); + assertTrue(node instanceof SingleNode); + assertEquals("A", node.getId()); + } + + private Task getTask(GraphNodeOptions graph, JsonObject actionNodeConfig, Vertx vertx) { + DefaultTaskFactoryConfig taskFactoryConfig = createTaskFactoryConfig(graph, actionNodeConfig); + return new DefaultTaskFactory().configure(taskFactoryConfig.toJson(), vertx) + .newInstance(SAMPLE_FRAGMENT_EVENT); + } + + private DefaultTaskFactoryConfig emptyFactoryConfig() { + return createTaskFactoryConfig(null, null); + } + + private DefaultTaskFactoryConfig createTaskFactoryConfig(GraphNodeOptions graph, + JsonObject actionNodeConfig) { + DefaultTaskFactoryConfig taskFactoryConfig = new DefaultTaskFactoryConfig(); + if (graph != null) { + taskFactoryConfig + .setTasks(Collections.singletonMap(TASK_NAME, graph)); + } + if (actionNodeConfig != null) { + List nodeFactories = Arrays.asList( + new NodeFactoryOptions().setFactory(ActionNodeFactory.NAME).setConfig(actionNodeConfig), + new NodeFactoryOptions().setFactory(SubtasksNodeFactory.NAME)); + taskFactoryConfig.setNodeFactories(nodeFactories); + } + return taskFactoryConfig; + } + + private JsonObject createActionNodeConfig(String actionName, String transition) { + return new ActionNodeFactoryConfig(Collections.singletonMap(actionName, + new ActionFactoryOptions(new JsonObject()) + .setFactory("test-action") + .setConfig(new JsonObject().put("transition", transition)))) + .toJson(); + } + + private JsonObject merge(JsonObject current, String actionName, String transition) { + JsonObject newOptions = createActionNodeConfig(actionName, transition); + return current.getJsonObject("actions").mergeIn(newOptions.getJsonObject("actions")); + } + + private List subTasks(GraphNodeOptions... nodes) { + return Arrays.asList(nodes); + } +} diff --git a/handler/core/src/test/java/io/knotx/fragments/task/factory/GraphNodeOptionsTest.java b/handler/core/src/test/java/io/knotx/fragments/task/factory/GraphNodeOptionsTest.java new file mode 100644 index 00000000..054b5ff4 --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/task/factory/GraphNodeOptionsTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory; + +import static io.knotx.fragments.HoconLoader.verify; +import static org.junit.jupiter.api.Assertions.*; + +import io.knotx.fragments.task.factory.node.action.ActionNodeConfig; +import io.knotx.fragments.task.factory.node.action.ActionNodeFactory; +import io.knotx.fragments.task.factory.node.subtasks.SubtasksNodeConfig; +import io.knotx.fragments.task.factory.node.subtasks.SubtasksNodeFactory; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxExtension; +import java.util.Optional; +import java.util.function.Consumer; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(VertxExtension.class) +class GraphNodeOptionsTest { + + @Test + @DisplayName("Expect task with action node when action defined directly in the task.") + void expectActionNodeWhenActionDirectlyDefinedInGraph(Vertx vertx) throws Throwable { + verify("task/factory/taskWithActionNode.conf", validateActionNode(), vertx); + } + + @Test + @DisplayName("Expect task with Action node when action is configured.") + void expectActionNodeWhenActionDefined(Vertx vertx) throws Throwable { + verify("task/factory/taskWithActionNode-fullSyntax.conf", validateActionNode(), vertx); + } + + @Test + @DisplayName("Expect subtasks node when actions is configured") + void expectSubTaskNodeWhenActionsDefined(Vertx vertx) throws Throwable { + verify("task/factory/taskWithSubtasksDeprecated.conf", validateSubtasksNode(), vertx); + } + + @Test + @DisplayName("Expect subtasks node when subtasks directly is configured") + void expectSubtasksNodeWhenSubtasksDirectlyDefined(Vertx vertx) throws Throwable { + verify("task/factory/taskWithSubtasks.conf", validateSubtasksNode(), vertx); + } + + @Test + @DisplayName("Expect subtasks node when subtasks is configured") + void expectSubtasksNodeWhenSubtasksDefined(Vertx vertx) throws Throwable { + verify("task/factory/taskWithSubtasks-fullSyntax.conf", validateSubtasksNode(), vertx); + } + + @Test + @DisplayName("Expect graph with nested composite nodes") + void expectDefaultGlobalLogLevel(Vertx vertx) throws Throwable { + verify("task/factory/taskWithSubtasks-fullSyntax.conf", validateSubtasksNode(), vertx); + } + + @Test + @DisplayName("Expect nodes configured with _success flow") + void expectTransitionSuccessWithNodeBThenNodeC(Vertx vertx) throws Throwable { + verify("task/factory/taskWithTransitions.conf", config -> { + GraphNodeOptions graphNodeOptions = new GraphNodeOptions(config); + Optional nodeB = graphNodeOptions.get("_success"); + assertTrue(nodeB.isPresent()); + assertEquals("b", getAction(nodeB.get())); + Optional nodeC = nodeB.get().get("_success"); + assertTrue(nodeC.isPresent()); + assertEquals("c", getAction(nodeC.get())); + }, vertx); + } + + + private Consumer validateActionNode() { + return config -> { + GraphNodeOptions graphNodeOptions = new GraphNodeOptions(config); + assertEquals("a", getAction(graphNodeOptions)); + assertEquals(ActionNodeFactory.NAME, graphNodeOptions.getNode().getFactory()); + }; + } + + private Consumer validateSubtasksNode() { + return config -> { + GraphNodeOptions graphNodeOptions = new GraphNodeOptions(config); + assertEquals(SubtasksNodeFactory.NAME, graphNodeOptions.getNode().getFactory()); + SubtasksNodeConfig nodeConfig = new SubtasksNodeConfig( + graphNodeOptions.getNode().getConfig()); + assertEquals(2, nodeConfig.getSubtasks().size()); + }; + } + + private String getAction(GraphNodeOptions graphNodeOptions) { + return new ActionNodeConfig( + graphNodeOptions.getNode().getConfig()).getAction(); + } +} \ No newline at end of file diff --git a/handler/core/src/test/java/io/knotx/fragments/task/factory/node/StubNode.java b/handler/core/src/test/java/io/knotx/fragments/task/factory/node/StubNode.java new file mode 100644 index 00000000..72befdbd --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/task/factory/node/StubNode.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory.node; + +import io.knotx.fragments.engine.graph.Node; +import io.knotx.fragments.engine.graph.NodeType; +import java.util.Optional; + +public class StubNode implements Node { + + private String id; + + public StubNode(String id) { + this.id = id; + } + + @Override + public String getId() { + return id; + } + + @Override + public Optional next(String transition) { + return Optional.empty(); + } + + @Override + public NodeType getType() { + return NodeType.SINGLE; + } +} diff --git a/handler/core/src/test/java/io/knotx/fragments/task/factory/node/action/ActionNodeFactoryConfigTest.java b/handler/core/src/test/java/io/knotx/fragments/task/factory/node/action/ActionNodeFactoryConfigTest.java new file mode 100644 index 00000000..1da9cc99 --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/task/factory/node/action/ActionNodeFactoryConfigTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory.node.action; + +import static io.knotx.fragments.HoconLoader.verify; + +import io.knotx.fragments.task.factory.LogLevelConfig; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxExtension; +import java.util.function.Consumer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(VertxExtension.class) +class ActionNodeFactoryConfigTest { + + @Test + @DisplayName("Expect all actions do not contain log level when global is not configured") + void expectNoLogLevel(Vertx vertx) throws Throwable { + verify("task/factory/node/action/actionNodeFactoryWithNoGlobalLogLevel.conf", + validateNoGlobalNodeLog(), vertx); + } + + @Test + @DisplayName("Expect all actions contain log level when global is configured") + void expectLogLevel(Vertx vertx) throws Throwable { + String expectedGlobalLogLevel = "INFO"; + verify("task/factory/node/action/actionNodeFactoryWithGlobalLogLevel.conf", + validateNodeLog(expectedGlobalLogLevel), vertx); + } + + @Test + @DisplayName("Expect local node log level is not overridden by global one") + void expectLocalLogLevel(Vertx vertx) throws Throwable { + String expectedLocalLogLevel = "ERROR"; + verify("task/factory/node/action/actionNodeFactoryWithLocalLogLevel.conf", + validateNodeLog(expectedLocalLogLevel), vertx); + } + + private Consumer validateNoGlobalNodeLog() { + return config -> { + ActionNodeFactoryConfig factoryConfig = new ActionNodeFactoryConfig(config); + factoryConfig.getActions().values().forEach( + actionOptions -> Assertions + .assertNull(new LogLevelConfig(actionOptions.getConfig()).getLogLevel()) + ); + }; + } + + private Consumer validateNodeLog(String logLevel) { + return config -> { + ActionNodeFactoryConfig factoryConfig = new ActionNodeFactoryConfig(config); + factoryConfig.getActions().values().forEach( + nodeFactoryOptions -> Assertions + .assertEquals(logLevel, nodeFactoryOptions.getConfig().getString("logLevel")) + ); + }; + } +} \ No newline at end of file diff --git a/handler/core/src/test/java/io/knotx/fragments/task/factory/node/action/ActionNodeFactoryTest.java b/handler/core/src/test/java/io/knotx/fragments/task/factory/node/action/ActionNodeFactoryTest.java new file mode 100644 index 00000000..86713ce2 --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/task/factory/node/action/ActionNodeFactoryTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory.node.action; + +import static io.knotx.fragments.handler.api.domain.FragmentResult.SUCCESS_TRANSITION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.knotx.fragments.engine.graph.Node; +import io.knotx.fragments.engine.graph.SingleNode; +import io.knotx.fragments.task.factory.ActionFactoryOptions; +import io.knotx.fragments.task.factory.node.StubNode; +import io.knotx.fragments.task.factory.GraphNodeOptions; +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxExtension; +import io.vertx.reactivex.core.Vertx; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(VertxExtension.class) +class ActionNodeFactoryTest { + + private static final Map NO_TRANSITIONS = Collections.emptyMap(); + + @Test + @DisplayName("Expect exception when `config.actions` not defined.") + void expectExceptionWhenActionsNotConfigured(Vertx vertx) { + // given + String actionAlias = "A"; + JsonObject config = new JsonObject(); + GraphNodeOptions graph = new GraphNodeOptions(actionAlias, NO_TRANSITIONS); + + // when, then + Assertions.assertThrows( + ActionNotFoundException.class, () -> new ActionNodeFactory().configure(config, vertx) + .initNode(graph, Collections.emptyMap(), null)); + } + + @Test + @DisplayName("Expect exception when action not found.") + void expectExceptionWhenActionNotFound(Vertx vertx) { + // given + String actionAlias = "A"; + JsonObject config = createNodeConfig("otherAction", SUCCESS_TRANSITION); + GraphNodeOptions graph = new GraphNodeOptions(actionAlias, NO_TRANSITIONS); + + // when, then + Assertions.assertThrows( + ActionNotFoundException.class, () -> new ActionNodeFactory().configure(config, vertx) + .initNode(graph, Collections.emptyMap(), null)); + } + + @Test + @DisplayName("Expect A node when action node defined.") + void expectSingleActionNode(Vertx vertx) { + // given + String actionAlias = "A"; + JsonObject config = createNodeConfig(actionAlias, SUCCESS_TRANSITION); + GraphNodeOptions graph = new GraphNodeOptions(actionAlias, NO_TRANSITIONS); + + // when + Node node = new ActionNodeFactory().configure(config, vertx) + .initNode(graph, Collections.emptyMap(), null); + + // then + assertEquals(actionAlias, node.getId()); + assertTrue(node instanceof SingleNode); + } + + @Test + @DisplayName("Expect node contains passed transitions.") + void expectActionNodesGraphWithTransition(Vertx vertx) { + // given + String actionAlias = "A"; + String transition = "transition"; + JsonObject config = createNodeConfig(actionAlias, SUCCESS_TRANSITION); + // this invalid configuration is expected + GraphNodeOptions graph = new GraphNodeOptions(actionAlias, Collections.emptyMap()); + + // when + Node node = new ActionNodeFactory().configure(config, vertx) + .initNode(graph, Collections.singletonMap(transition, new StubNode("B")), null); + + // then + Optional nextNode = node.next(transition); + assertTrue(nextNode.isPresent()); + assertEquals("B", nextNode.get().getId()); + } + + private JsonObject createNodeConfig(String actionName, String transition) { + return new ActionNodeFactoryConfig(Collections.singletonMap(actionName, + new ActionFactoryOptions(new JsonObject()) + .setFactory("test-action") + .setConfig(new JsonObject().put("transition", transition)))) + .toJson(); + } +} \ No newline at end of file diff --git a/handler/core/src/test/java/io/knotx/fragments/task/factory/node/subtasks/SubtasksNodeFactoryTest.java b/handler/core/src/test/java/io/knotx/fragments/task/factory/node/subtasks/SubtasksNodeFactoryTest.java new file mode 100644 index 00000000..28de86b5 --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/task/factory/node/subtasks/SubtasksNodeFactoryTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2019 Knot.x Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.knotx.fragments.task.factory.node.subtasks; + +import static io.knotx.fragments.handler.api.domain.FragmentResult.ERROR_TRANSITION; +import static io.knotx.fragments.handler.api.domain.FragmentResult.SUCCESS_TRANSITION; +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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.knotx.fragments.engine.graph.CompositeNode; +import io.knotx.fragments.engine.graph.Node; +import io.knotx.fragments.task.factory.NodeProvider; +import io.knotx.fragments.task.factory.node.NodeOptions; +import io.knotx.fragments.task.factory.node.StubNode; +import io.knotx.fragments.task.factory.GraphNodeOptions; +import io.vertx.core.json.JsonObject; +import io.vertx.junit5.VertxExtension; +import io.vertx.reactivex.core.Vertx; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(VertxExtension.class) +class SubtasksNodeFactoryTest { + + private static final Map NO_TRANSITIONS = Collections.emptyMap(); + + @Test + @DisplayName("Expect composite node with single subgraph.") + void expectCompositeNodeW(Vertx vertx) { + // given + GraphNodeOptions subNodeConfig = new GraphNodeOptions( + new NodeOptions("someFactory", new JsonObject()), + NO_TRANSITIONS + ); + + GraphNodeOptions graph = new GraphNodeOptions( + subTasks(subNodeConfig), + NO_TRANSITIONS + ); + + NodeProvider nodeProvider = mock(NodeProvider.class); + when(nodeProvider.initNode(eq(subNodeConfig))).thenReturn(new StubNode("A")); + + // when + Node node = new SubtasksNodeFactory().configure(new JsonObject(), vertx) + .initNode(graph, Collections.emptyMap(), nodeProvider); + + // then + assertTrue(node instanceof CompositeNode); + assertEquals(SubtasksNodeFactory.COMPOSITE_NODE_ID, node.getId()); + assertFalse(node.next(SUCCESS_TRANSITION).isPresent()); + assertFalse(node.next(ERROR_TRANSITION).isPresent()); + + CompositeNode compositeRootNode = (CompositeNode) node; + assertEquals(1, compositeRootNode.getNodes().size()); + Node subNode = compositeRootNode.getNodes().get(0); + assertEquals("A", subNode.getId()); + } + + @Test + @DisplayName("Expect only _success and _error transitions.") + void expectOnlySuccessAndErrorTransitions(Vertx vertx) { + // given + GraphNodeOptions anyNodeConfig = new GraphNodeOptions(new JsonObject()); + + GraphNodeOptions graph = new GraphNodeOptions( + subTasks(anyNodeConfig), + NO_TRANSITIONS + ); + + NodeProvider nodeProvider = mock(NodeProvider.class); + when(nodeProvider.initNode(any())).thenReturn(new StubNode("A")); + + Map transitionsToNodes = new HashMap<>(); + transitionsToNodes.put(SUCCESS_TRANSITION, new StubNode("B")); + transitionsToNodes.put(ERROR_TRANSITION, new StubNode("C")); + transitionsToNodes.put("otherTransition", new StubNode("D")); + + // when + Node node = new SubtasksNodeFactory().configure(new JsonObject(), vertx) + .initNode(graph, transitionsToNodes, nodeProvider); + + // then + assertTrue(node.next(SUCCESS_TRANSITION).isPresent()); + assertEquals("B", node.next(SUCCESS_TRANSITION).get().getId()); + assertTrue(node.next(ERROR_TRANSITION).isPresent()); + assertEquals("C", node.next(ERROR_TRANSITION).get().getId()); + assertFalse(node.next("otherTransition").isPresent()); + } + + @Test + @DisplayName("Expect graph with nested composite nodes") + void expectNestedCompositeNodesGraph(Vertx vertx) { + // given + GraphNodeOptions nestedNodeConfig = new GraphNodeOptions( + subTasks( + new GraphNodeOptions(new JsonObject()) + ), NO_TRANSITIONS); + + GraphNodeOptions graph = new GraphNodeOptions( + subTasks( + nestedNodeConfig + ), NO_TRANSITIONS + ); + + NodeProvider nodeProvider = mock(NodeProvider.class); + + // when + new SubtasksNodeFactory().configure(new JsonObject(), vertx) + .initNode(graph, Collections.emptyMap(), nodeProvider); + + // then + verify(nodeProvider, times(1)).initNode(eq(nestedNodeConfig)); + + } + + private List subTasks(GraphNodeOptions... nodes) { + return Arrays.asList(nodes); + } +} \ No newline at end of file diff --git a/handler/core/src/test/java/io/knotx/fragments/task/options/TaskOptionsTest.java b/handler/core/src/test/java/io/knotx/fragments/task/options/TaskOptionsTest.java deleted file mode 100644 index d480e513..00000000 --- a/handler/core/src/test/java/io/knotx/fragments/task/options/TaskOptionsTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (C) 2019 Knot.x Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.knotx.fragments.task.options; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.knotx.fragments.task.ConfigurationTaskProviderFactory; -import io.vertx.config.ConfigRetriever; -import io.vertx.config.ConfigRetrieverOptions; -import io.vertx.config.ConfigStoreOptions; -import io.vertx.core.AsyncResult; -import io.vertx.core.Handler; -import io.vertx.core.Vertx; -import io.vertx.core.json.JsonObject; -import io.vertx.junit5.VertxExtension; -import io.vertx.junit5.VertxTestContext; -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -@ExtendWith(VertxExtension.class) -class TaskOptionsTest { - - @Test - @DisplayName("Expect configuration Task provider when factory not defined") - void expectDefaultTaskProvider(Vertx vertx) throws Throwable { - verify("tasks/defaultTaskProvider.conf", config -> { - TaskOptions options = new TaskOptions(config); - assertEquals(ConfigurationTaskProviderFactory.NAME, options.getFactory()); - assertTrue(options.getConfig().isEmpty()); - }, vertx); - } - - @Test - @DisplayName("Expect custom Task provider with configuration when factory and config defined.") - void expectCustomTaskProvider(Vertx vertx) throws Throwable { - verify("tasks/customTaskProvider.conf", config -> { - TaskOptions options = new TaskOptions(config); - assertEquals("custom", options.getFactory()); - assertEquals(new JsonObject().put("anyKey", "anyValue"), options.getConfig()); - }, vertx); - } - - @Test - @DisplayName("Expect task with action node when simplified task definition.") - void expectActionNodeWhenSimplifiedTask(Vertx vertx) throws Throwable { - validateActionNode("tasks/taskSimplified.conf", vertx); - } - - @Test - @DisplayName("Expect task with action node when action defined directly in the task.") - void expectActionNodeWhenActionDirectlyDefinedInGraph(Vertx vertx) throws Throwable { - validateActionNode("tasks/taskWithActionNode.conf", vertx); - } - - @Test - @DisplayName("Expect task with Action node when action is configured.") - void expectActionNodeWhenActionDefined(Vertx vertx) throws Throwable { - validateActionNode("tasks/taskWithActionNode-fullSyntax.conf", vertx); - } - - @Test - @DisplayName("Expect subtasks node when actions is configured") - void expectSubTaskNodeWhenActionsDefined(Vertx vertx) throws Throwable { - validateSubtasksNode("tasks/taskWithSubtasksDeprecated.conf", vertx); - } - - @Test - @DisplayName("Expect subtasks node when subtasks directly is configured") - void expectSubtasksNodeWhenSubtasksDirectlyDefined(Vertx vertx) throws Throwable { - validateSubtasksNode("tasks/taskWithSubtasks.conf", vertx); - } - - @Test - @DisplayName("Expect subtasks node when subtasks is configured") - void expectSubtasksNodeWhenSubtasksDefined(Vertx vertx) throws Throwable { - validateSubtasksNode("tasks/taskWithSubtasks-fullSyntax.conf", vertx); - } - - @Test - @DisplayName("Expect nodes configured with _success flow") - void expectTransitionSuccessWithNodeBThenNodeC(Vertx vertx) throws Throwable { - verify("tasks/taskWithTransitions.conf", config -> { - GraphNodeOptions graphNodeOptions = new TaskOptions(config).getGraph(); - Optional nodeB = graphNodeOptions.get("_success"); - assertTrue(nodeB.isPresent()); - assertEquals("b", getAction(nodeB.get())); - Optional nodeC = nodeB.get().get("_success"); - assertTrue(nodeC.isPresent()); - assertEquals("c", getAction(nodeC.get())); - }, vertx); - } - - private void validateActionNode(String file, Vertx vertx) throws Throwable { - verify(file, config -> { - GraphNodeOptions graphNodeOptions = new TaskOptions(config).getGraph(); - assertEquals("a", getAction(graphNodeOptions)); - assertEquals(GraphNodeOptions.ACTION, graphNodeOptions.getNode().getFactory()); - }, vertx); - } - - private void validateSubtasksNode(String file, Vertx vertx) throws Throwable { - verify(file, config -> { - TaskOptions taskOptions = new TaskOptions(config); - GraphNodeOptions graphNodeOptions = taskOptions.getGraph(); - assertEquals(GraphNodeOptions.SUBTASKS, graphNodeOptions.getNode().getFactory()); - SubtasksNodeConfigOptions subtasks = new SubtasksNodeConfigOptions( - graphNodeOptions.getNode().getConfig()); - assertEquals(2, subtasks.getSubtasks().size()); - }, vertx); - } - - private String getAction(GraphNodeOptions graphNodeOptions) { - return new ActionNodeConfigOptions( - graphNodeOptions.getNode().getConfig()).getAction(); - } - - void verify(String fileName, Consumer assertions, Vertx vertx) throws Throwable { - VertxTestContext testContext = new VertxTestContext(); - Handler> configHandler = testContext - .succeeding(config -> testContext.verify(() -> { - assertions.accept(config); - testContext.completeNow(); - })); - fromHOCON(fileName, vertx, configHandler); - - Assertions.assertTrue(testContext.awaitCompletion(5, TimeUnit.SECONDS)); - if (testContext.failed()) { - throw testContext.causeOfFailure(); - } - } - - private void fromHOCON(String fileName, Vertx vertx, - Handler> configHandler) { - ConfigRetrieverOptions options = new ConfigRetrieverOptions(); - options.addStore(new ConfigStoreOptions() - .setType("file") - .setFormat("hocon") - .setConfig(new JsonObject().put("path", fileName))); - - ConfigRetriever retriever = ConfigRetriever.create(vertx, options); - retriever.getConfig(configHandler); - } - -} \ No newline at end of file diff --git a/handler/core/src/test/resources/META-INF/services/io.knotx.fragments.task.TaskProviderFactory b/handler/core/src/test/resources/META-INF/services/io.knotx.fragments.task.TaskFactory similarity index 91% rename from handler/core/src/test/resources/META-INF/services/io.knotx.fragments.task.TaskProviderFactory rename to handler/core/src/test/resources/META-INF/services/io.knotx.fragments.task.TaskFactory index 5afade95..82553982 100644 --- a/handler/core/src/test/resources/META-INF/services/io.knotx.fragments.task.TaskProviderFactory +++ b/handler/core/src/test/resources/META-INF/services/io.knotx.fragments.task.TaskFactory @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -io.knotx.fragments.task.ConfigurationTaskProviderFactory \ No newline at end of file +io.knotx.fragments.task.factory.DefaultTaskFactory \ No newline at end of file diff --git a/handler/core/src/test/resources/handler/common/customTask.conf b/handler/core/src/test/resources/handler/common/customTask.conf new file mode 100644 index 00000000..853bd933 --- /dev/null +++ b/handler/core/src/test/resources/handler/common/customTask.conf @@ -0,0 +1,25 @@ +tasks { + success-task { + action = success-action + } +} + +nodeFactories = [ + { + factory = action + config { + actions { + success-action { + factory = test-action + config { + transition = _success + body = "custom" + } + } + } + } + } + { + factory = subtasks + } +] \ No newline at end of file diff --git a/handler/core/src/test/resources/handler/common/failingTask.conf b/handler/core/src/test/resources/handler/common/failingTask.conf new file mode 100644 index 00000000..46a94974 --- /dev/null +++ b/handler/core/src/test/resources/handler/common/failingTask.conf @@ -0,0 +1,24 @@ +tasks { + failing-task { + action = failing-action + } +} + +nodeFactories = [ + { + factory = action + config { + actions { + failing-action { + factory = test-action + config { + transition = not-existing-transition + } + } + } + } + } + { + factory = subtasks + } +] \ No newline at end of file diff --git a/handler/core/src/test/resources/handler/common/successTask.conf b/handler/core/src/test/resources/handler/common/successTask.conf new file mode 100644 index 00000000..0b561976 --- /dev/null +++ b/handler/core/src/test/resources/handler/common/successTask.conf @@ -0,0 +1,25 @@ +tasks { + success-task { + action = success-action + } +} + +nodeFactories = [ + { + factory = action + config { + actions { + success-action { + factory = test-action + config { + transition = _success + body = "success" + } + } + } + } + } + { + factory = subtasks + } +] \ No newline at end of file diff --git a/handler/core/src/test/resources/handler/manyTaskFactoriesWithTheSameName.conf b/handler/core/src/test/resources/handler/manyTaskFactoriesWithTheSameName.conf new file mode 100644 index 00000000..a104a973 --- /dev/null +++ b/handler/core/src/test/resources/handler/manyTaskFactoriesWithTheSameName.conf @@ -0,0 +1,13 @@ +taskFactories = [ + { + factory = default + config {include required(classpath("handler/common/successTask.conf"))} + } + { + factory = default + config {include required(classpath("handler/common/customTask.conf"))} + config { + taskNameKey = task + } + } +] \ No newline at end of file diff --git a/handler/core/src/test/resources/handler/singleTaskFactoryWithFailingTask.conf b/handler/core/src/test/resources/handler/singleTaskFactoryWithFailingTask.conf new file mode 100644 index 00000000..7ec10ca1 --- /dev/null +++ b/handler/core/src/test/resources/handler/singleTaskFactoryWithFailingTask.conf @@ -0,0 +1,6 @@ +taskFactories = [ + { + factory = default + config { include required(classpath("handler/common/failingTask.conf")) } + } +] \ No newline at end of file diff --git a/handler/core/src/test/resources/handler/singleTaskFactoryWithSuccessTask.conf b/handler/core/src/test/resources/handler/singleTaskFactoryWithSuccessTask.conf new file mode 100644 index 00000000..ea3e4bb7 --- /dev/null +++ b/handler/core/src/test/resources/handler/singleTaskFactoryWithSuccessTask.conf @@ -0,0 +1,6 @@ +taskFactories = [ + { + factory = default + config { include required(classpath("handler/common/successTask.conf")) } + } +] \ No newline at end of file diff --git a/handler/core/src/test/resources/handler/taskFactoryNameNotDefined.conf b/handler/core/src/test/resources/handler/taskFactoryNameNotDefined.conf new file mode 100644 index 00000000..4e222c46 --- /dev/null +++ b/handler/core/src/test/resources/handler/taskFactoryNameNotDefined.conf @@ -0,0 +1,4 @@ +taskFactories = [ + { + } +] \ No newline at end of file diff --git a/handler/core/src/test/resources/handler/taskFactoryNotFound.conf b/handler/core/src/test/resources/handler/taskFactoryNotFound.conf new file mode 100644 index 00000000..79cdce2a --- /dev/null +++ b/handler/core/src/test/resources/handler/taskFactoryNotFound.conf @@ -0,0 +1,5 @@ +taskFactories = [ + { + factory = invalid + } +] \ No newline at end of file diff --git a/handler/core/src/test/resources/task/factory/node/action/actionNodeFactoryWithGlobalLogLevel.conf b/handler/core/src/test/resources/task/factory/node/action/actionNodeFactoryWithGlobalLogLevel.conf new file mode 100644 index 00000000..e3f4a381 --- /dev/null +++ b/handler/core/src/test/resources/task/factory/node/action/actionNodeFactoryWithGlobalLogLevel.conf @@ -0,0 +1,10 @@ +logLevel = INFO + +actions { + test { + factory = "factory" + config { + + } + } +} \ No newline at end of file diff --git a/handler/core/src/test/resources/task/factory/node/action/actionNodeFactoryWithLocalLogLevel.conf b/handler/core/src/test/resources/task/factory/node/action/actionNodeFactoryWithLocalLogLevel.conf new file mode 100644 index 00000000..86602c5d --- /dev/null +++ b/handler/core/src/test/resources/task/factory/node/action/actionNodeFactoryWithLocalLogLevel.conf @@ -0,0 +1,10 @@ +logLevel = INFO + +actions { + test { + factory = "factory" + config { + logLevel = ERROR + } + } +} \ No newline at end of file diff --git a/handler/core/src/test/resources/task/factory/node/action/actionNodeFactoryWithNoGlobalLogLevel.conf b/handler/core/src/test/resources/task/factory/node/action/actionNodeFactoryWithNoGlobalLogLevel.conf new file mode 100644 index 00000000..064e70f8 --- /dev/null +++ b/handler/core/src/test/resources/task/factory/node/action/actionNodeFactoryWithNoGlobalLogLevel.conf @@ -0,0 +1,8 @@ +actions { + test { + factory = "factory" + config { + + } + } +} \ No newline at end of file diff --git a/handler/core/src/test/resources/task/factory/taskFactoryWithGlobalLogLevel.conf b/handler/core/src/test/resources/task/factory/taskFactoryWithGlobalLogLevel.conf new file mode 100644 index 00000000..db3a6da6 --- /dev/null +++ b/handler/core/src/test/resources/task/factory/taskFactoryWithGlobalLogLevel.conf @@ -0,0 +1,10 @@ +logLevel = INFO + +nodeFactories = [ + { + factory = action + } + { + factory = subtasks + } +] \ No newline at end of file diff --git a/handler/core/src/test/resources/task/factory/taskFactoryWithLocalLogLevel.conf b/handler/core/src/test/resources/task/factory/taskFactoryWithLocalLogLevel.conf new file mode 100644 index 00000000..ea4a052d --- /dev/null +++ b/handler/core/src/test/resources/task/factory/taskFactoryWithLocalLogLevel.conf @@ -0,0 +1,10 @@ +logLevel = INFO + +nodeFactories = [ + { + factory = action + config { + logLevel = ERROR + } + } +] \ No newline at end of file diff --git a/handler/core/src/test/resources/task/factory/taskFactoryWithNoGlobalLogLevel.conf b/handler/core/src/test/resources/task/factory/taskFactoryWithNoGlobalLogLevel.conf new file mode 100644 index 00000000..a0799c3f --- /dev/null +++ b/handler/core/src/test/resources/task/factory/taskFactoryWithNoGlobalLogLevel.conf @@ -0,0 +1,5 @@ +nodeFactories = [ + { + factory = action + } +] \ No newline at end of file diff --git a/handler/core/src/test/resources/task/factory/taskWithActionNode-fullSyntax.conf b/handler/core/src/test/resources/task/factory/taskWithActionNode-fullSyntax.conf new file mode 100644 index 00000000..f7653336 --- /dev/null +++ b/handler/core/src/test/resources/task/factory/taskWithActionNode-fullSyntax.conf @@ -0,0 +1,9 @@ +# node & transitions +node { + factory = action + config { + action = a + } +} +onTransitions { +} \ No newline at end of file diff --git a/handler/core/src/test/resources/tasks/defaultTaskProvider.conf b/handler/core/src/test/resources/task/factory/taskWithActionNode.conf similarity index 67% rename from handler/core/src/test/resources/tasks/defaultTaskProvider.conf rename to handler/core/src/test/resources/task/factory/taskWithActionNode.conf index d312fbd8..9f7bab3e 100644 --- a/handler/core/src/test/resources/tasks/defaultTaskProvider.conf +++ b/handler/core/src/test/resources/task/factory/taskWithActionNode.conf @@ -1,3 +1,2 @@ # node & transitions -graph { -} \ No newline at end of file +action = a \ No newline at end of file diff --git a/handler/core/src/test/resources/task/factory/taskWithSubtasks-fullSyntax.conf b/handler/core/src/test/resources/task/factory/taskWithSubtasks-fullSyntax.conf new file mode 100644 index 00000000..7008fde0 --- /dev/null +++ b/handler/core/src/test/resources/task/factory/taskWithSubtasks-fullSyntax.conf @@ -0,0 +1,14 @@ +# node & transitions +node { + factory = subtasks + config { + subtasks = [ + { + action = a + }, + { + action = b + } + ] + } +} \ No newline at end of file diff --git a/handler/core/src/test/resources/task/factory/taskWithSubtasks.conf b/handler/core/src/test/resources/task/factory/taskWithSubtasks.conf new file mode 100644 index 00000000..a821ae3b --- /dev/null +++ b/handler/core/src/test/resources/task/factory/taskWithSubtasks.conf @@ -0,0 +1,9 @@ +# node & transitions +subtasks = [ + { + action = a + }, + { + action = b + } +] \ No newline at end of file diff --git a/handler/core/src/test/resources/tasks/taskWithSubtasksDeprecated.conf b/handler/core/src/test/resources/task/factory/taskWithSubtasksDeprecated.conf similarity index 97% rename from handler/core/src/test/resources/tasks/taskWithSubtasksDeprecated.conf rename to handler/core/src/test/resources/task/factory/taskWithSubtasksDeprecated.conf index 9b1877b1..c3e7348f 100644 --- a/handler/core/src/test/resources/tasks/taskWithSubtasksDeprecated.conf +++ b/handler/core/src/test/resources/task/factory/taskWithSubtasksDeprecated.conf @@ -6,4 +6,4 @@ actions = [ { action = b } -] +] \ No newline at end of file diff --git a/handler/core/src/test/resources/task/factory/taskWithTransitions.conf b/handler/core/src/test/resources/task/factory/taskWithTransitions.conf new file mode 100644 index 00000000..f71c214a --- /dev/null +++ b/handler/core/src/test/resources/task/factory/taskWithTransitions.conf @@ -0,0 +1,12 @@ +# node & transitions +action = a +onTransitions { + _success { + action = b + onTransitions { + _success { + action = c + } + } + } +} \ No newline at end of file diff --git a/handler/core/src/test/resources/tasks/customTaskProvider.conf b/handler/core/src/test/resources/tasks/customTaskProvider.conf deleted file mode 100644 index 9e951e4b..00000000 --- a/handler/core/src/test/resources/tasks/customTaskProvider.conf +++ /dev/null @@ -1,8 +0,0 @@ -# task factory -factory = custom -config { - anyKey = anyValue -} -# node & transitions -graph { -} \ No newline at end of file diff --git a/handler/core/src/test/resources/tasks/fragment-handler/failingAction.json b/handler/core/src/test/resources/tasks/fragment-handler/failingAction.json deleted file mode 100644 index 3ff980f6..00000000 --- a/handler/core/src/test/resources/tasks/fragment-handler/failingAction.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "tasks": - { - "failing-task": { - "graph": { - "action": "failing-action" - } - } - }, - "actions": - { - "failing-action": { - "factory": "test-action", - "config" : { - "transition": "not-existing-transition" - } - } - } -} \ No newline at end of file diff --git a/handler/core/src/test/resources/tasks/fragment-handler/successAction.json b/handler/core/src/test/resources/tasks/fragment-handler/successAction.json deleted file mode 100644 index e93549ca..00000000 --- a/handler/core/src/test/resources/tasks/fragment-handler/successAction.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "tasks": - { - "success-task": { - "graph": { - "action": "success-action" - } - } - }, - "actions": - { - "success-action": { - "factory": "test-action", - "config" : { - "transition": "_success" - } - } - } -} \ No newline at end of file diff --git a/handler/core/src/test/resources/tasks/taskSimplified.conf b/handler/core/src/test/resources/tasks/taskSimplified.conf deleted file mode 100644 index 6f67f535..00000000 --- a/handler/core/src/test/resources/tasks/taskSimplified.conf +++ /dev/null @@ -1,5 +0,0 @@ -# node & transitions -action = a - - - diff --git a/handler/core/src/test/resources/tasks/taskWithActionNode-fullSyntax.conf b/handler/core/src/test/resources/tasks/taskWithActionNode-fullSyntax.conf deleted file mode 100644 index 24299b10..00000000 --- a/handler/core/src/test/resources/tasks/taskWithActionNode-fullSyntax.conf +++ /dev/null @@ -1,11 +0,0 @@ -# node & transitions -graph { - node { - factory = action - config { - action = a - } - } - onTransitions { - } -} \ No newline at end of file diff --git a/handler/core/src/test/resources/tasks/taskWithActionNode.conf b/handler/core/src/test/resources/tasks/taskWithActionNode.conf deleted file mode 100644 index 3dcf57d8..00000000 --- a/handler/core/src/test/resources/tasks/taskWithActionNode.conf +++ /dev/null @@ -1,8 +0,0 @@ -# node & transitions -graph { - action = a -} - - - - diff --git a/handler/core/src/test/resources/tasks/taskWithSubtasks-fullSyntax.conf b/handler/core/src/test/resources/tasks/taskWithSubtasks-fullSyntax.conf deleted file mode 100644 index 2acd745c..00000000 --- a/handler/core/src/test/resources/tasks/taskWithSubtasks-fullSyntax.conf +++ /dev/null @@ -1,16 +0,0 @@ -# node & transitions -graph { - node { - factory = subtasks - config { - subtasks = [ - { - action = a - }, - { - action = b - } - ] - } - } -} \ No newline at end of file diff --git a/handler/core/src/test/resources/tasks/taskWithSubtasks.conf b/handler/core/src/test/resources/tasks/taskWithSubtasks.conf deleted file mode 100644 index 573231b2..00000000 --- a/handler/core/src/test/resources/tasks/taskWithSubtasks.conf +++ /dev/null @@ -1,11 +0,0 @@ -# node & transitions -graph { - subtasks = [ - { - action = a - }, - { - action = b - } - ] -} diff --git a/handler/core/src/test/resources/tasks/taskWithTransitions.conf b/handler/core/src/test/resources/tasks/taskWithTransitions.conf deleted file mode 100644 index a8b3f50a..00000000 --- a/handler/core/src/test/resources/tasks/taskWithTransitions.conf +++ /dev/null @@ -1,14 +0,0 @@ -# node & transitions -graph { - action = a - onTransitions { - _success { - action = b - onTransitions { - _success { - action = c - } - } - } - } -} \ No newline at end of file diff --git a/handler/engine/README.md b/handler/engine/README.md index edbd709c..7023b406 100644 --- a/handler/engine/README.md +++ b/handler/engine/README.md @@ -1,12 +1,47 @@ # Fragments Engine -Fragments Engine is a reactive asynchronous map-reduce implementation, enjoying the benefits of Reactive Extensions, -that evaluates each Fragment independently using a `Task` definition. `Task` specifies a directed graph of Nodes, -allowing to transform Fragment into the new one. +Fragments Engine is a [reactive](https://www.reactivemanifesto.org/) asynchronous +[map-reduce](https://en.wikipedia.org/wiki/MapReduce) implementation, enjoying the benefits of +[Reactive Extensions](http://reactivex.io/), that evaluates each +[Fragment](https://github.com/Knotx/knotx-fragments/tree/master/api) independently using a +[Task](#task) definition. Task specifies Nodes through which Fragments will be routed by +(a directed graph of [Nodes](#node)), allowing to transform Fragment into the new one. ## How does it work -Any *Fragment* can define its processing path - a **Task** (which is a **directed graph** of **Nodes**). -A **Task** specifies the nodes through which Fragments will be routed by the Task Engine. -Each Node may define possible *outgoing edges* - **Transitions**. +Fragments engine accepts a list of fragments decorated with its [status](#fragments-status), +[processing log](#fragments-status) and the incoming HTTP request data. All operations, nodes +evaluations and graph executions are asynchronous. So the engine responds with RX handler that +enables the user to press the play button (with the `io.reactivex.SingleSource.subscribe` method). +This is where map-reduce magic begins. + +When the user calls the `subscribe` method, then all fragments are evaluated in parallel. Finally, +we get a list of modified fragments that contain also their statuses and processing log. + +The engine handles all exceptions launched by nodes with a graph logic. It translates errors to +`_error` transitions and tries to manage them.\ +All `io.knotx.fragments.handler.api.exception.NodeFatalException` exceptions break the processing +of all fragments and call the `io.reactivex.SingleObserver#onError` method to indicate that all +fragments have failed. Then the client can handle this situation. + +## How to configure +The engine is stateless, so no configuration is required. The clients provide their custom +configurations and build tasks. Tasks contain all details about graph processing. + +# Task +Task decomposes business logic into lightweight independent parts. Those parts are graph nodes +connected by transitions. So a task is a directed graph of nodes. Nodes specify fragment's +processing, whereas transitions draw business decisions. +This graph is acyclic and each of its nodes can be reached only from exactly one path (transition). +These properties enable us to treat the Task as a tree structure. +``` +(A) ───> (B) ───> (C) + └─────> (D) +``` +The above graph contains nodes, represented by `(A)`, and transitions illustrated as arrows. Arrows +set a node's contract. + +So, for example, a node can invoke API and store the response in the fragment. Then it +responds with the modified fragment and transition. + ## Node The node responsibility can be described as: @@ -15,11 +50,8 @@ The node responsibility can be described as: > transition and L is a node log. The node definition is abstract. It allows to define simple processing nodes but also more complex -structures such as a list of subgraphs. - -The node definition is abstract. It allows to define simple processing nodes but also more complex -structures such as a list of subgraphs. Furthermore, such a definition inspires to provide custom -node implementations. +structures such as a list of subgraphs. Each node may define possible outgoing edges, called +transitions. ### Node types There are two **node** types: @@ -68,6 +100,9 @@ There are two important rules to remember: > If a node responds with a not configured transition, the "Unsupported Transition" error occurs. +Nodes can declare custom transitions. Custom transitions allow to react to non standard situations +such as data sources timeouts, fallbacks etc. + ## Fragment's status During fragment's processing, a fragment's status is calculated. Each node responds with a transition. Fragments Engine validates node responses and set one of the fragment's statuses: diff --git a/handler/engine/src/main/java/io/knotx/fragments/engine/graph/CompositeNode.java b/handler/engine/src/main/java/io/knotx/fragments/engine/graph/CompositeNode.java index 27696beb..93bad3cd 100644 --- a/handler/engine/src/main/java/io/knotx/fragments/engine/graph/CompositeNode.java +++ b/handler/engine/src/main/java/io/knotx/fragments/engine/graph/CompositeNode.java @@ -19,6 +19,7 @@ import static io.knotx.fragments.handler.api.domain.FragmentResult.ERROR_TRANSITION; import java.util.List; +import java.util.Objects; import java.util.Optional; public class CompositeNode implements Node { @@ -60,6 +61,26 @@ public List getNodes() { return nodes; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CompositeNode that = (CompositeNode) o; + return Objects.equals(id, that.id) && + Objects.equals(nodes, that.nodes) && + Objects.equals(onSuccess, that.onSuccess) && + Objects.equals(onError, that.onError); + } + + @Override + public int hashCode() { + return Objects.hash(id, nodes, onSuccess, onError); + } + @Override public String toString() { return "CompositeNode{" + diff --git a/handler/engine/src/main/java/io/knotx/fragments/engine/graph/SingleNode.java b/handler/engine/src/main/java/io/knotx/fragments/engine/graph/SingleNode.java index d3f69dd0..0f82512a 100644 --- a/handler/engine/src/main/java/io/knotx/fragments/engine/graph/SingleNode.java +++ b/handler/engine/src/main/java/io/knotx/fragments/engine/graph/SingleNode.java @@ -19,6 +19,7 @@ import io.knotx.fragments.handler.api.domain.FragmentResult; import io.reactivex.Single; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; @@ -60,9 +61,28 @@ public Single doAction(FragmentContext fragmentContext) { return action.apply(fragmentContext); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SingleNode that = (SingleNode) o; + return Objects.equals(id, that.id) && + Objects.equals(action, that.action) && + Objects.equals(transitions, that.transitions); + } + + @Override + public int hashCode() { + return Objects.hash(id, action, transitions); + } + @Override public String toString() { - return "ActionNode{" + + return "SingleNode{" + "id='" + id + '\'' + ", action=" + action + ", transitions=" + transitions +