From ffa686694c7095cfaa655e6643eac9ca51f1ebbd Mon Sep 17 00:00:00 2001 From: Marcin <8897707+marcinus@users.noreply.github.com> Date: Tue, 10 Mar 2020 21:39:15 +0100 Subject: [PATCH] #92 Provide task metadata to event consumer. Expose in debug script. (#104) #92 Provide task metadata to event consumer. Expose in debug script. Co-authored-by: marcin.szymura Co-authored-by: Tomasz Michalak Co-authored-by: Voycawojka --- ...ircuitBreakerActionFactoryOptionsTest.java | 3 +- .../cb/CircuitBreakerActionFactoryTest.java | 3 +- handler/core/docs/asciidoc/dataobjects.adoc | 57 ++ .../knotx/fragments/engine/NodeMetadata.java | 92 +++ .../fragments/engine/OperationMetadata.java | 50 ++ .../knotx/fragments/engine/TaskMetadata.java | 66 +++ .../fragments/engine/TaskWithMetadata.java | 45 ++ .../knotx/fragments/engine/TasksMetadata.java | 38 ++ .../fragments/handler/ExecutionPlan.java | 92 +++ .../fragments/handler/FragmentsHandler.java | 63 +-- .../fragments/handler/LoggedNodeStatus.java | 74 +++ .../knotx/fragments/handler/TaskProvider.java | 6 +- .../consumer/FragmentEventsConsumer.java | 12 +- .../consumer/html/FragmentExecutionLog.java | 117 ++++ .../FragmentHtmlBodyWriterFactory.java | 36 +- .../consumer/html/GraphNodeExecutionLog.java | 185 +++++++ .../consumer/html/GraphNodeOperationLog.java | 95 ++++ .../consumer/html/GraphNodeResponseLog.java | 93 ++++ .../consumer/metadata/EventLogConverter.java | 86 +++ .../consumer/metadata/MetadataConverter.java | 121 ++++ .../consumer/metadata/NodeExecutionData.java | 61 ++ .../io/knotx/fragments/task/TaskFactory.java | 30 + .../task/factory/DefaultTaskFactory.java | 34 +- .../fragments/task/factory/NodeProvider.java | 29 +- .../task/factory/node/NodeFactory.java | 18 + .../node/action/ActionNodeFactory.java | 55 +- .../node/action/ActionNodeFactoryConfig.java | 14 +- .../factory/node/action/ActionProvider.java | 3 +- .../node/subtasks/SubtasksNodeFactory.java | 41 +- ...ler.consumer.FragmentEventsConsumerFactory | 2 +- .../handler/FragmentsHandlerTest.java | 27 +- .../handler/LoggedNodeStatusTest.java | 115 ++++ .../knotx/fragments/handler/TestAction.java | 1 - .../html/FragmentExecutionLogTest.java | 70 +++ .../FragmentHtmlBodyWriterFactoryTest.java | 121 +++- .../html/GraphNodeExecutionLogTest.java | 78 +++ .../html/GraphNodeOperationLogTest.java | 54 ++ .../html/GraphNodeResponseLogTest.java | 43 ++ .../metadata/EventLogConverterTest.java | 184 ++++++ .../metadata/MetadataConverterTest.java | 524 ++++++++++++++++++ .../task/factory/DefaultTaskFactoryTest.java | 320 +++++++---- .../task/factory/GraphNodeOptionsTest.java | 8 +- .../node/action/ActionNodeFactoryTest.java | 223 ++++++-- .../subtasks/SubtasksNodeFactoryTest.java | 192 +++++-- .../io/knotx/fragments/engine/EventLog.java | 25 +- .../knotx/fragments/engine/EventLogEntry.java | 35 +- .../fragments/engine/FragmentsEngine.java | 2 +- .../engine/TaskExecutionContext.java | 8 +- 48 files changed, 3318 insertions(+), 333 deletions(-) create mode 100644 handler/core/src/main/java/io/knotx/fragments/engine/NodeMetadata.java create mode 100644 handler/core/src/main/java/io/knotx/fragments/engine/OperationMetadata.java create mode 100644 handler/core/src/main/java/io/knotx/fragments/engine/TaskMetadata.java create mode 100644 handler/core/src/main/java/io/knotx/fragments/engine/TaskWithMetadata.java create mode 100644 handler/core/src/main/java/io/knotx/fragments/engine/TasksMetadata.java create mode 100644 handler/core/src/main/java/io/knotx/fragments/handler/ExecutionPlan.java create mode 100644 handler/core/src/main/java/io/knotx/fragments/handler/LoggedNodeStatus.java create mode 100644 handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/FragmentExecutionLog.java rename handler/core/src/main/java/io/knotx/fragments/handler/consumer/{ => html}/FragmentHtmlBodyWriterFactory.java (71%) create mode 100644 handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/GraphNodeExecutionLog.java create mode 100644 handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/GraphNodeOperationLog.java create mode 100644 handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/GraphNodeResponseLog.java create mode 100644 handler/core/src/main/java/io/knotx/fragments/handler/consumer/metadata/EventLogConverter.java create mode 100644 handler/core/src/main/java/io/knotx/fragments/handler/consumer/metadata/MetadataConverter.java create mode 100644 handler/core/src/main/java/io/knotx/fragments/handler/consumer/metadata/NodeExecutionData.java create mode 100644 handler/core/src/test/java/io/knotx/fragments/handler/LoggedNodeStatusTest.java create mode 100644 handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/FragmentExecutionLogTest.java rename handler/core/src/test/java/io/knotx/fragments/handler/consumer/{ => html}/FragmentHtmlBodyWriterFactoryTest.java (63%) create mode 100644 handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/GraphNodeExecutionLogTest.java create mode 100644 handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/GraphNodeOperationLogTest.java create mode 100644 handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/GraphNodeResponseLogTest.java create mode 100644 handler/core/src/test/java/io/knotx/fragments/handler/consumer/metadata/EventLogConverterTest.java create mode 100644 handler/core/src/test/java/io/knotx/fragments/handler/consumer/metadata/MetadataConverterTest.java diff --git a/handler/actions/src/test/java/io/knotx/fragments/handler/action/cb/CircuitBreakerActionFactoryOptionsTest.java b/handler/actions/src/test/java/io/knotx/fragments/handler/action/cb/CircuitBreakerActionFactoryOptionsTest.java index cd7dde7f..dc149ee2 100644 --- a/handler/actions/src/test/java/io/knotx/fragments/handler/action/cb/CircuitBreakerActionFactoryOptionsTest.java +++ b/handler/actions/src/test/java/io/knotx/fragments/handler/action/cb/CircuitBreakerActionFactoryOptionsTest.java @@ -15,6 +15,7 @@ */ package io.knotx.fragments.handler.action.cb; +import static io.knotx.fragments.engine.api.node.single.FragmentResult.ERROR_TRANSITION; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -70,6 +71,6 @@ void expectErrorTransitionConfigured() { CircuitBreakerActionFactoryOptions tested = new CircuitBreakerActionFactoryOptions(json); // then - assertTrue(tested.getErrorTransitions().contains("_error")); + assertTrue(tested.getErrorTransitions().contains(ERROR_TRANSITION)); } } \ No newline at end of file diff --git a/handler/actions/src/test/java/io/knotx/fragments/handler/action/cb/CircuitBreakerActionFactoryTest.java b/handler/actions/src/test/java/io/knotx/fragments/handler/action/cb/CircuitBreakerActionFactoryTest.java index fdb5dece..b5c281bf 100644 --- a/handler/actions/src/test/java/io/knotx/fragments/handler/action/cb/CircuitBreakerActionFactoryTest.java +++ b/handler/actions/src/test/java/io/knotx/fragments/handler/action/cb/CircuitBreakerActionFactoryTest.java @@ -15,6 +15,7 @@ */ package io.knotx.fragments.handler.action.cb; +import static io.knotx.fragments.engine.api.node.single.FragmentResult.ERROR_TRANSITION; import static io.knotx.fragments.handler.action.cb.CircuitBreakerAction.ERROR_LOG_KEY; import static io.knotx.fragments.handler.action.cb.CircuitBreakerAction.INVOCATION_COUNT_LOG_KEY; import static io.knotx.fragments.handler.action.cb.CircuitBreakerActionFactory.FALLBACK_TRANSITION; @@ -679,7 +680,7 @@ private void validateScenario(Action firstInvocationBehaviour, Action secondInvo CircuitBreakerAction tested = new CircuitBreakerAction(circuitBreaker, CircuitBreakerDoActions .applyOneAfterAnother(firstInvocationBehaviour, secondInvocationBehaviour), "tested", - INFO, Collections.singleton("_error")); + INFO, Collections.singleton(ERROR_TRANSITION)); // when tested.apply(new FragmentContext(FRAGMENT, new ClientRequest()), handler); diff --git a/handler/core/docs/asciidoc/dataobjects.adoc b/handler/core/docs/asciidoc/dataobjects.adoc index fcc9b2ae..c77f64bf 100644 --- a/handler/core/docs/asciidoc/dataobjects.adoc +++ b/handler/core/docs/asciidoc/dataobjects.adoc @@ -106,6 +106,21 @@ The factory name. +++ |=== +[[FragmentExecutionLog]] +== FragmentExecutionLog + + +[cols=">25%,25%,50%"] +[frame="topbot"] +|=== +^|Name | Type ^| Description +|[[finishTime]]`@finishTime`|`Number (long)`|- +|[[fragment]]`@fragment`|`link:dataobjects.html#Fragment[Fragment]`|- +|[[graph]]`@graph`|`link:dataobjects.html#GraphNodeExecutionLog[GraphNodeExecutionLog]`|- +|[[startTime]]`@startTime`|`Number (long)`|- +|[[status]]`@status`|`link:enums.html#Status[Status]`|- +|=== + [[FragmentsHandlerOptions]] == FragmentsHandlerOptions @@ -127,6 +142,36 @@ The array/list of task factory options defines factories taking part in the crea +++ |=== +[[GraphNodeExecutionLog]] +== GraphNodeExecutionLog + + +[cols=">25%,25%,50%"] +[frame="topbot"] +|=== +^|Name | Type ^| Description +|[[id]]`@id`|`String`|- +|[[label]]`@label`|`String`|- +|[[on]]`@on`|`link:dataobjects.html#GraphNodeExecutionLog[GraphNodeExecutionLog]`|- +|[[operation]]`@operation`|`link:dataobjects.html#GraphNodeOperationLog[GraphNodeOperationLog]`|- +|[[response]]`@response`|`link:dataobjects.html#GraphNodeResponseLog[GraphNodeResponseLog]`|- +|[[status]]`@status`|`link:enums.html#LoggedNodeStatus[LoggedNodeStatus]`|- +|[[subtasks]]`@subtasks`|`Array of link:dataobjects.html#GraphNodeExecutionLog[GraphNodeExecutionLog]`|- +|[[type]]`@type`|`link:enums.html#NodeType[NodeType]`|- +|=== + +[[GraphNodeOperationLog]] +== GraphNodeOperationLog + + +[cols=">25%,25%,50%"] +[frame="topbot"] +|=== +^|Name | Type ^| Description +|[[data]]`@data`|`Json object`|- +|[[factory]]`@factory`|`String`|- +|=== + [[GraphNodeOptions]] == GraphNodeOptions @@ -157,6 +202,18 @@ Sets a node factory name to SubtasksNodeFactory.NAME and configures +++ |=== +[[GraphNodeResponseLog]] +== GraphNodeResponseLog + + +[cols=">25%,25%,50%"] +[frame="topbot"] +|=== +^|Name | Type ^| Description +|[[invocations]]`@invocations`|`Json array`|- +|[[transition]]`@transition`|`String`|- +|=== + [[LogLevelConfig]] == LogLevelConfig diff --git a/handler/core/src/main/java/io/knotx/fragments/engine/NodeMetadata.java b/handler/core/src/main/java/io/knotx/fragments/engine/NodeMetadata.java new file mode 100644 index 00000000..c0f5b6eb --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/engine/NodeMetadata.java @@ -0,0 +1,92 @@ +/* + * 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.engine; + +import io.knotx.fragments.engine.api.node.NodeType; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class NodeMetadata { + + private String nodeId; + private String label; + private NodeType type; + private Map transitions; + private List nestedNodes; + private OperationMetadata operation; + + public static NodeMetadata single(String nodeId, String label, Map transitions, OperationMetadata operation) { + return new NodeMetadata(nodeId, label, NodeType.SINGLE, transitions, Collections.emptyList(), operation); + } + + public static NodeMetadata composite(String nodeId, String label, Map transitions, + List nestedNodes, OperationMetadata operation) { + return new NodeMetadata(nodeId, label, NodeType.COMPOSITE, transitions, nestedNodes, operation); + } + + private NodeMetadata(String nodeId, String label, NodeType type, Map transitions, + List nestedNodes, OperationMetadata operation) { + this.nodeId = nodeId; + this.label = label; + this.type = type; + this.transitions = transitions; + this.nestedNodes = nestedNodes; + this.operation = operation; + } + + public String getNodeId() { + return nodeId; + } + + public String getLabel() { + return label; + } + + public NodeType getType() { + return type; + } + + /** + * @return transition name to node id map + */ + public Map getTransitions() { + return transitions; + } + + /** + * @return list of composite nodes identifiers + */ + public List getNestedNodes() { + return nestedNodes; + } + + public OperationMetadata getOperation() { + return operation; + } + + @Override + public String toString() { + return "NodeMetadata{" + + "nodeId='" + nodeId + '\'' + + ", label='" + label + '\'' + + ", type=" + type + + ", transitions=" + transitions + + ", nestedNodes=" + nestedNodes + + ", operation=" + operation + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/engine/OperationMetadata.java b/handler/core/src/main/java/io/knotx/fragments/engine/OperationMetadata.java new file mode 100644 index 00000000..d8cc31c8 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/engine/OperationMetadata.java @@ -0,0 +1,50 @@ +/* + * 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.engine; + +import io.vertx.core.json.JsonObject; + +public class OperationMetadata { + + private final String factory; + + private final JsonObject data; + + public OperationMetadata(String factory) { + this(factory, new JsonObject()); + } + + public OperationMetadata(String factory, JsonObject data) { + this.factory = factory; + this.data = data; + } + + public String getFactory() { + return factory; + } + + public JsonObject getData() { + return data; + } + + @Override + public String toString() { + return "OperationMetadata{" + + "factory='" + factory + '\'' + + ", data=" + data + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/engine/TaskMetadata.java b/handler/core/src/main/java/io/knotx/fragments/engine/TaskMetadata.java new file mode 100644 index 00000000..72c34cc1 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/engine/TaskMetadata.java @@ -0,0 +1,66 @@ +/* + * 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.engine; + +import io.knotx.fragments.handler.ExecutionPlan; +import java.util.HashMap; +import java.util.Map; + +public class TaskMetadata { + + private final String taskName; + private final String rootNodeId; + private final Map nodesMetadata; + + private TaskMetadata(String taskName, String rootNodeId, Map nodesMetadata) { + this.taskName = taskName; + this.rootNodeId = rootNodeId; + this.nodesMetadata = nodesMetadata; + } + + public static TaskMetadata create(String taskName, String rootNodeId, Map nodesMetadata) { + return new TaskMetadata(taskName, rootNodeId, nodesMetadata); + } + + public static TaskMetadata noMetadata(String taskName, String rootNodeId) { + return new TaskMetadata(taskName, rootNodeId, new HashMap<>()); + } + + public static TaskMetadata notDefined() { + return noMetadata(ExecutionPlan.UNDEFINED_TASK, ""); + } + + public String getTaskName() { + return taskName; + } + + public String getRootNodeId() { + return rootNodeId; + } + + public Map getNodesMetadata() { + return nodesMetadata; + } + + @Override + public String toString() { + return "TaskMetadata{" + + "taskName='" + taskName + '\'' + + ", rootNodeId='" + rootNodeId + '\'' + + ", nodesMetadata=" + nodesMetadata + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/engine/TaskWithMetadata.java b/handler/core/src/main/java/io/knotx/fragments/engine/TaskWithMetadata.java new file mode 100644 index 00000000..b4c951d3 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/engine/TaskWithMetadata.java @@ -0,0 +1,45 @@ +/* + * 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.engine; + +import io.knotx.fragments.engine.api.Task; + +public class TaskWithMetadata { + + private final Task task; + private final TaskMetadata metadata; + + public TaskWithMetadata(Task task, TaskMetadata taskMetadata) { + this.task = task; + this.metadata = taskMetadata; + } + + public Task getTask() { + return task; + } + + public TaskMetadata getMetadata() { + return metadata; + } + + @Override + public String toString() { + return "TaskWithMetadata{" + + "task=" + task + + ", taskMetadata=" + metadata + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/engine/TasksMetadata.java b/handler/core/src/main/java/io/knotx/fragments/engine/TasksMetadata.java new file mode 100644 index 00000000..a1cb40de --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/engine/TasksMetadata.java @@ -0,0 +1,38 @@ +/* + * 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.engine; + +import java.util.Map; + +public class TasksMetadata { + + private final Map tasksMetadataByFragmentId; + + public TasksMetadata(Map tasksMetadataByFragmentId) { + this.tasksMetadataByFragmentId = tasksMetadataByFragmentId; + } + + public TaskMetadata get(String fragmentId) { + return tasksMetadataByFragmentId.get(fragmentId); + } + + @Override + public String toString() { + return "TasksMetadata{" + + "tasksMetadataByFragmentId=" + tasksMetadataByFragmentId + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/ExecutionPlan.java b/handler/core/src/main/java/io/knotx/fragments/handler/ExecutionPlan.java new file mode 100644 index 00000000..ec16fe5a --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/handler/ExecutionPlan.java @@ -0,0 +1,92 @@ +/* + * 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.api.Fragment; +import io.knotx.fragments.engine.*; +import io.knotx.fragments.engine.api.Task; +import io.knotx.server.api.context.ClientRequest; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ExecutionPlan { + + public static final String UNDEFINED_TASK = "_NOT_DEFINED"; + + private final TaskProvider taskProvider; + private final Map plan; + + ExecutionPlan(List fragments, ClientRequest clientRequest, TaskProvider taskProvider) { + this.taskProvider = taskProvider; + this.plan = fragments.stream() + .map(fragment -> new FragmentEventContext(new FragmentEvent(fragment), clientRequest)) + .collect(Collectors.toMap(context -> context, + this::getTaskWithMetadataFor, + (u, v) -> { + throw new IllegalStateException(String.format("Duplicate key %s", u)); + }, + LinkedHashMap::new + )); + } + + public Stream getEntryStream() { + return plan.entrySet().stream() + .map(entry -> new Entry(entry.getKey(), entry.getValue())); + } + + public TasksMetadata getTasksMetadata() { + return new TasksMetadata(getEntryStream() + .collect(Collectors.toMap( + entry -> entry.getContext().getFragmentEvent().getFragment().getId(), + entry -> entry.getTaskWithMetadata().getMetadata()))); + } + + private TaskWithMetadata getTaskWithMetadataFor(FragmentEventContext fragmentEventContext) { + return taskProvider.newInstance(fragmentEventContext) + .orElseGet(() -> new TaskWithMetadata(new Task(UNDEFINED_TASK), TaskMetadata.notDefined())); + } + + public static class Entry { + + private final FragmentEventContext context; + private final TaskWithMetadata taskWithMetadata; + + private Entry(FragmentEventContext context, TaskWithMetadata taskWithMetadata) { + this.context = context; + this.taskWithMetadata = taskWithMetadata; + } + + public FragmentEventContext getContext() { + return context; + } + + public TaskWithMetadata getTaskWithMetadata() { + return taskWithMetadata; + } + + @Override + public String toString() { + return "Entry{" + + "context=" + context + + ", taskWithMetadata=" + taskWithMetadata + + '}'; + } + } +} 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 b21c03fd..9deabd87 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 @@ -18,12 +18,8 @@ package io.knotx.fragments.handler; import io.knotx.fragments.api.Fragment; -import io.knotx.fragments.engine.FragmentEvent; +import io.knotx.fragments.engine.*; import io.knotx.fragments.engine.FragmentEvent.Status; -import io.knotx.fragments.engine.FragmentEventContext; -import io.knotx.fragments.engine.FragmentEventContextTaskAware; -import io.knotx.fragments.engine.FragmentsEngine; -import io.knotx.fragments.engine.api.Task; import io.knotx.fragments.handler.consumer.FragmentEventsConsumerProvider; import io.knotx.server.api.context.ClientRequest; import io.knotx.server.api.context.RequestContext; @@ -38,12 +34,14 @@ import io.vertx.core.logging.LoggerFactory; import io.vertx.reactivex.core.Vertx; import io.vertx.reactivex.ext.web.RoutingContext; + import java.util.List; import java.util.function.Predicate; import java.util.stream.Collectors; public class FragmentsHandler implements Handler { + // TODO add some logging here private static final Logger LOGGER = LoggerFactory.getLogger(FragmentsHandler.class); private final RequestContextEngine requestContextEngine; @@ -67,10 +65,12 @@ public void handle(RoutingContext routingContext) { final List fragments = routingContext.get("fragments"); final ClientRequest clientRequest = requestContext.getRequestEvent().getClientRequest(); - Single> doHandle = doHandle(fragments, clientRequest); - doHandle + ExecutionPlan executionPlan = new ExecutionPlan(fragments, clientRequest, taskProvider); + + doHandle(executionPlan) + .doOnError(e -> LOGGER.error("Fragments not processed correctly!", e)) .doOnSuccess(events -> putFragments(routingContext, events)) - .doOnSuccess(events -> enrichWithEventConsumers(clientRequest, events)) + .doOnSuccess(events -> enrichWithEventConsumers(clientRequest, events, executionPlan)) .map(events -> toHandlerResult(events, requestContext)) .subscribe( result -> requestContextEngine @@ -79,17 +79,23 @@ public void handle(RoutingContext routingContext) { ); } - protected Single> doHandle(List fragments, - ClientRequest clientRequest) { - return Single.just(fragments) - .map(f -> toEvents(f, clientRequest)) - .flatMap(engine::execute); + public ExecutionPlan createExecutionPlan(List fragments, ClientRequest clientRequest) { + return new ExecutionPlan(fragments, clientRequest, taskProvider); + } + + protected Single> doHandle(ExecutionPlan executionPlan) { + return engine.execute(executionPlan.getEntryStream() + .peek(entry -> LOGGER.info("Processing task [{}]", entry.getTaskWithMetadata())) + .map(entry -> new FragmentEventContextTaskAware(entry.getTaskWithMetadata().getTask(), + entry.getContext())) + .collect(Collectors.toList())); } - private void enrichWithEventConsumers(ClientRequest clientRequest, - List fragmentEvents) { + private void enrichWithEventConsumers(ClientRequest clientRequest, List events, + ExecutionPlan executionPlan) { + TasksMetadata tasksMetadata = executionPlan.getTasksMetadata(); fragmentEventsConsumerProvider.provide() - .forEach(consumer -> consumer.accept(clientRequest, fragmentEvents)); + .forEach(consumer -> consumer.accept(clientRequest, events, tasksMetadata)); } private void putFragments(RoutingContext routingContext, List events) { @@ -130,29 +136,4 @@ private List retrieveFragments(List events, .map(FragmentEvent::getFragment) .collect(Collectors.toList()); } - - 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 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"), - fragmentEventContext)); - }) - .collect( - Collectors.toList()); - } - } diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/LoggedNodeStatus.java b/handler/core/src/main/java/io/knotx/fragments/handler/LoggedNodeStatus.java new file mode 100644 index 00000000..f9fe1ca4 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/handler/LoggedNodeStatus.java @@ -0,0 +1,74 @@ +/* + * 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 static io.knotx.fragments.engine.EventLogEntry.NodeStatus; +import static io.knotx.fragments.engine.api.node.single.FragmentResult.ERROR_TRANSITION; +import static io.knotx.fragments.engine.api.node.single.FragmentResult.SUCCESS_TRANSITION; + +import io.knotx.fragments.engine.EventLogEntry; + +import java.util.Arrays; +import org.apache.commons.lang3.StringUtils; + +public enum LoggedNodeStatus { + SUCCESS { + @Override + protected boolean isEquivalent(NodeStatus status, String transition) { + return SUCCESS_TRANSITION.equals(transition); + } + }, + + ERROR { + @Override + protected boolean isEquivalent(NodeStatus status, String transition) { + return ERROR_TRANSITION.equals(transition) || status == NodeStatus.TIMEOUT; + } + }, + + OTHER { + @Override + protected boolean isEquivalent(NodeStatus status, String transition) { + return StringUtils.isNotEmpty(transition) && !SUCCESS_TRANSITION.equals(transition) + && !ERROR_TRANSITION.equals(transition) && status != NodeStatus.UNSUPPORTED_TRANSITION; + } + }, + + MISSING { + @Override + protected boolean isEquivalent(NodeStatus status, String transition) { + return false; + } + }, + + UNPROCESSED { + @Override + protected boolean isEquivalent(NodeStatus status, String transition) { + return status == NodeStatus.UNPROCESSED; + } + }; + + public static LoggedNodeStatus from(EventLogEntry logEntry) { + return Arrays.stream(LoggedNodeStatus.values()) + .filter(status -> status.isEquivalent(logEntry.getStatus(), logEntry.getTransition())) + .findAny() + .orElseThrow((() -> new IllegalArgumentException( + String.format("Cannot find LoggedNodeStatus for NodeStatus=%s and transition=%s", + logEntry.getStatus(), logEntry.getTransition())))); + } + + protected abstract boolean isEquivalent(NodeStatus status, String transition); +} 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 index 076462c8..47e53610 100644 --- a/handler/core/src/main/java/io/knotx/fragments/handler/TaskProvider.java +++ b/handler/core/src/main/java/io/knotx/fragments/handler/TaskProvider.java @@ -16,7 +16,7 @@ package io.knotx.fragments.handler; import io.knotx.fragments.engine.FragmentEventContext; -import io.knotx.fragments.engine.api.Task; +import io.knotx.fragments.engine.TaskWithMetadata; import io.knotx.fragments.handler.exception.TaskFactoryNotFoundException; import io.knotx.fragments.spi.FactoryOptions; import io.knotx.fragments.task.TaskFactory; @@ -43,7 +43,7 @@ class TaskProvider { factories = initFactories(factoryOptions); } - Optional newInstance(FragmentEventContext eventContext) { + Optional newInstance(FragmentEventContext eventContext) { return factories.stream() .filter(f -> f.accept(eventContext)) .findFirst() @@ -52,7 +52,7 @@ Optional newInstance(FragmentEventContext eventContext) { eventContext.getFragmentEvent().getFragment().getId()); return f; }) - .map(f -> f.newInstance(eventContext)); + .map(f -> f.newInstanceWithMetadata(eventContext)); } private List initFactories(List optionsList) { diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/consumer/FragmentEventsConsumer.java b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/FragmentEventsConsumer.java index 4f47e411..f13420ee 100644 --- a/handler/core/src/main/java/io/knotx/fragments/handler/consumer/FragmentEventsConsumer.java +++ b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/FragmentEventsConsumer.java @@ -15,14 +15,15 @@ */ package io.knotx.fragments.handler.consumer; -import io.knotx.fragments.engine.api.Task; +import io.knotx.fragments.engine.FragmentEvent; +import io.knotx.fragments.engine.TaskMetadata; +import io.knotx.fragments.engine.TasksMetadata; import io.knotx.server.api.context.ClientRequest; import java.util.List; - -import io.knotx.fragments.engine.FragmentEvent; +import java.util.Map; /** - * Fragment event consumer receives {@link FragmentEvent} when {@link Task} + * Fragment event consumer receives {@link FragmentEvent} when {@link io.knotx.fragments.engine.api.Task} * evaluation ends. It can share this information with some external tools or even modify fragment. */ public interface FragmentEventsConsumer { @@ -32,7 +33,8 @@ public interface FragmentEventsConsumer { * * @param clientRequest - client request * @param fragmentEvents - all fragment events + * @param tasksMetadata - mapping from fragment id to associated task's metadata */ - void accept(ClientRequest clientRequest, List fragmentEvents); + void accept(ClientRequest clientRequest, List fragmentEvents, TasksMetadata tasksMetadata); } diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/FragmentExecutionLog.java b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/FragmentExecutionLog.java new file mode 100644 index 00000000..f5d6d94e --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/FragmentExecutionLog.java @@ -0,0 +1,117 @@ +/* + * 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.consumer.html; + +import io.knotx.fragments.api.Fragment; +import io.knotx.fragments.engine.FragmentEvent; +import io.knotx.fragments.engine.FragmentEvent.Status; +import io.vertx.codegen.annotations.DataObject; +import io.vertx.core.json.JsonObject; + +@DataObject(generateConverter = true) +public class FragmentExecutionLog { + + private Fragment fragment; + private FragmentEvent.Status status = Status.UNPROCESSED; + private long startTime = 0; + private long finishTime = 0; + private GraphNodeExecutionLog graph = null; + + public static FragmentExecutionLog newInstance(FragmentEvent fragmentEvent, + GraphNodeExecutionLog graph) { + return newInstance(fragmentEvent) + .setGraph(graph); + } + + public static FragmentExecutionLog newInstance(FragmentEvent fragmentEvent) { + return new FragmentExecutionLog() + .setFragment(fragmentEvent.getFragment()) + .setStatus(fragmentEvent.getStatus()) + .setStartTime(fragmentEvent.getLog().getEarliestTimestamp()) + .setFinishTime(fragmentEvent.getLog().getLatestTimestamp()); + } + + public FragmentExecutionLog() { + // default constructor + } + + public FragmentExecutionLog(JsonObject jsonObject) { + FragmentExecutionLogConverter.fromJson(jsonObject, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + FragmentExecutionLogConverter.toJson(this, json); + return json; + } + + public Fragment getFragment() { + return fragment; + } + + public FragmentExecutionLog setFragment(Fragment fragment) { + this.fragment = fragment; + return this; + } + + public Status getStatus() { + return status; + } + + public FragmentExecutionLog setStatus(Status status) { + this.status = status; + return this; + } + + public long getStartTime() { + return startTime; + } + + public FragmentExecutionLog setStartTime(long startTime) { + this.startTime = startTime; + return this; + } + + public long getFinishTime() { + return finishTime; + } + + public FragmentExecutionLog setFinishTime(long finishTime) { + this.finishTime = finishTime; + return this; + } + + public GraphNodeExecutionLog getGraph() { + return graph; + } + + public FragmentExecutionLog setGraph( + GraphNodeExecutionLog graph) { + this.graph = graph; + return this; + } + + @Override + public String toString() { + return "FragmentExecutionLog{" + + "status=" + status + + ", startTime=" + startTime + + ", finishTime=" + finishTime + + ", fragment=" + fragment + + ", graph=" + graph + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/consumer/FragmentHtmlBodyWriterFactory.java b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/FragmentHtmlBodyWriterFactory.java similarity index 71% rename from handler/core/src/main/java/io/knotx/fragments/handler/consumer/FragmentHtmlBodyWriterFactory.java rename to handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/FragmentHtmlBodyWriterFactory.java index 72e50a46..6456045a 100644 --- a/handler/core/src/main/java/io/knotx/fragments/handler/consumer/FragmentHtmlBodyWriterFactory.java +++ b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/FragmentHtmlBodyWriterFactory.java @@ -13,10 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.knotx.fragments.handler.consumer; +package io.knotx.fragments.handler.consumer.html; import io.knotx.fragments.api.Fragment; import io.knotx.fragments.engine.FragmentEvent; +import io.knotx.fragments.engine.TasksMetadata; +import io.knotx.fragments.handler.consumer.FragmentEventsConsumer; +import io.knotx.fragments.handler.consumer.FragmentEventsConsumerFactory; +import io.knotx.fragments.handler.consumer.metadata.MetadataConverter; import io.knotx.server.api.context.ClientRequest; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -32,7 +36,8 @@ public class FragmentHtmlBodyWriterFactory implements FragmentEventsConsumerFact static final String FRAGMENT_TYPES_OPTIONS = "fragmentTypes"; static final String CONDITION_OPTION = "condition"; static final String HEADER_OPTION = "header"; - static final String PARAM_OPTION = "param"; + + private static final String PARAM_OPTION = "param"; @Override public String getName() { @@ -47,26 +52,37 @@ public FragmentEventsConsumer create(JsonObject config) { private String requestParam = getConditionParam(config); @Override - public void accept(ClientRequest request, List events) { + public void accept(ClientRequest request, List events, + TasksMetadata tasksMetadata) { if (containsHeader(request) || containsParam(request)) { events.stream() .filter(this::isSupported) - .forEach(this::wrapFragmentBody); + .forEach(event -> wrapFragmentBodyWithMetadata(event, tasksMetadata)); } } - private void wrapFragmentBody(FragmentEvent fragmentEvent) { - Fragment fragment = fragmentEvent.getFragment(); + private void wrapFragmentBodyWithMetadata(FragmentEvent event, TasksMetadata tasksMetadata) { + FragmentExecutionLog executionLog = + Optional.ofNullable(tasksMetadata.get(event.getFragment().getId())) + .map(metadata -> new MetadataConverter(event, metadata)) + .map(MetadataConverter::getExecutionLog) + .map(graphLog -> FragmentExecutionLog.newInstance(event, graphLog)) + .orElseGet(()-> FragmentExecutionLog.newInstance(event)); + + wrapFragmentBody(event.getFragment(), executionLog.toJson()); + } + + private void wrapFragmentBody(Fragment fragment, JsonObject log) { fragment.setBody("" - + addAsScript(fragmentEvent) + + logAsScript(fragment.getId(), log) + fragment.getBody() + ""); } - private String addAsScript(FragmentEvent event) { - return ""; } diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/GraphNodeExecutionLog.java b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/GraphNodeExecutionLog.java new file mode 100644 index 00000000..70809993 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/GraphNodeExecutionLog.java @@ -0,0 +1,185 @@ +/* + * 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.consumer.html; + +import io.knotx.fragments.engine.api.node.NodeType; +import io.knotx.fragments.handler.LoggedNodeStatus; +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; + +@DataObject(generateConverter = true) +public class GraphNodeExecutionLog { + + private String id; + private NodeType type = NodeType.SINGLE; + private String label = StringUtils.EMPTY; + private List subtasks = new ArrayList<>(); + private GraphNodeOperationLog operation = GraphNodeOperationLog.empty(); + private Map on = new HashMap<>(); + + private LoggedNodeStatus status = LoggedNodeStatus.SUCCESS; + private GraphNodeResponseLog response = new GraphNodeResponseLog(); + + + public static GraphNodeExecutionLog newInstance(String id) { + return new GraphNodeExecutionLog().setId(id); + } + + public static GraphNodeExecutionLog newInstance(String id, NodeType type, String label, + List subtasks, GraphNodeOperationLog operation, + Map on) { + return new GraphNodeExecutionLog() + .setId(id) + .setType(type) + .setLabel(label) + .setSubtasks(subtasks) + .setOperation(operation) + .setOn(on) + .setResponse(new GraphNodeResponseLog()); + } + + public GraphNodeExecutionLog() { + // default constructor; + } + + public GraphNodeExecutionLog(JsonObject jsonObject) { + GraphNodeExecutionLogConverter.fromJson(jsonObject, this); + } + + public JsonObject toJson() { + JsonObject result = new JsonObject(); + GraphNodeExecutionLogConverter.toJson(this, result); + return result; + } + + public String getId() { + return id; + } + + public GraphNodeExecutionLog setId(String id) { + this.id = id; + return this; + } + + public NodeType getType() { + return type; + } + + public GraphNodeExecutionLog setType(NodeType type) { + this.type = type; + return this; + } + + public String getLabel() { + return label; + } + + public GraphNodeExecutionLog setLabel(String label) { + this.label = label; + return this; + } + + public List getSubtasks() { + return subtasks; + } + + public GraphNodeExecutionLog setSubtasks( + List subtasks) { + this.subtasks = subtasks; + return this; + } + + public GraphNodeOperationLog getOperation() { + return operation; + } + + public GraphNodeExecutionLog setOperation( + GraphNodeOperationLog operation) { + this.operation = operation; + return this; + } + + public Map getOn() { + return on; + } + + public GraphNodeExecutionLog setOn(Map on) { + this.on = on; + return this; + } + + public LoggedNodeStatus getStatus() { + return status; + } + + public GraphNodeExecutionLog setStatus(LoggedNodeStatus status) { + this.status = status; + return this; + } + + public GraphNodeResponseLog getResponse() { + return response; + } + + public GraphNodeExecutionLog setResponse(GraphNodeResponseLog response) { + this.response = response; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GraphNodeExecutionLog that = (GraphNodeExecutionLog) o; + return Objects.equals(id, that.id) && + type == that.type && + Objects.equals(label, that.label) && + Objects.equals(subtasks, that.subtasks) && + Objects.equals(operation, that.operation) && + Objects.equals(on, that.on) && + status == that.status && + Objects.equals(response, that.response); + } + + @Override + public int hashCode() { + return Objects.hash(id, type, label, subtasks, operation, on, status, response); + } + + @Override + public String toString() { + return "GraphNodeExecutionLog{" + + "id='" + id + '\'' + + ", type=" + type + + ", label='" + label + '\'' + + ", subtasks=" + subtasks + + ", operation=" + operation + + ", on=" + on + + ", status=" + status + + ", response=" + response + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/GraphNodeOperationLog.java b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/GraphNodeOperationLog.java new file mode 100644 index 00000000..d8972094 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/GraphNodeOperationLog.java @@ -0,0 +1,95 @@ +/* + * 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.consumer.html; + +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 GraphNodeOperationLog { + + private String factory; + + private JsonObject data; + + public static GraphNodeOperationLog empty() { + return new GraphNodeOperationLog().setFactory(StringUtils.EMPTY).setData(new JsonObject()); + } + + public static GraphNodeOperationLog newInstance(String factory, JsonObject data) { + return new GraphNodeOperationLog().setFactory(factory).setData(data); + } + + public GraphNodeOperationLog() { + // default constructor + } + + public GraphNodeOperationLog(JsonObject obj) { + GraphNodeOperationLogConverter.fromJson(obj, this); + } + + public JsonObject toJson() { + JsonObject json = new JsonObject(); + GraphNodeOperationLogConverter.toJson(this, json); + return json; + } + + public String getFactory() { + return factory; + } + + public GraphNodeOperationLog setFactory(String factory) { + this.factory = factory; + return this; + } + + public JsonObject getData() { + return data; + } + + public GraphNodeOperationLog setData(JsonObject data) { + this.data = data; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GraphNodeOperationLog that = (GraphNodeOperationLog) o; + return Objects.equals(factory, that.factory) && + Objects.equals(data, that.data); + } + + @Override + public int hashCode() { + return Objects.hash(factory, data); + } + + @Override + public String toString() { + return "OperationMetadata{" + + "factory='" + factory + '\'' + + ", data=" + data + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/GraphNodeResponseLog.java b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/GraphNodeResponseLog.java new file mode 100644 index 00000000..e1817f78 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/html/GraphNodeResponseLog.java @@ -0,0 +1,93 @@ +/* + * 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.consumer.html; + +import io.vertx.codegen.annotations.DataObject; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import java.util.Objects; + +@DataObject(generateConverter = true) +public class GraphNodeResponseLog { + + private String transition; + private JsonArray invocations; + + public static GraphNodeResponseLog newInstance(String transition, JsonArray invocations) { + return new GraphNodeResponseLog() + .setTransition(transition) + .setInvocations(invocations); + } + + public GraphNodeResponseLog() { + // default constructor + } + + public GraphNodeResponseLog(JsonObject json) { + // default constructor + GraphNodeResponseLogConverter.fromJson(json, this); + } + + public JsonObject toJson() { + JsonObject result = new JsonObject(); + GraphNodeResponseLogConverter.toJson(this, result); + return result; + } + + public String getTransition() { + return transition; + } + + public GraphNodeResponseLog setTransition(String transition) { + this.transition = transition; + return this; + } + + public JsonArray getInvocations() { + return invocations; + } + + public GraphNodeResponseLog setInvocations(JsonArray invocations) { + this.invocations = invocations; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GraphNodeResponseLog that = (GraphNodeResponseLog) o; + return Objects.equals(transition, that.transition) && + Objects.equals(invocations, that.invocations); + } + + @Override + public int hashCode() { + return Objects.hash(transition, invocations); + } + + @Override + public String toString() { + return "GraphNodeResponseLog{" + + "transition='" + transition + '\'' + + ", invocations=" + invocations + + '}'; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/consumer/metadata/EventLogConverter.java b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/metadata/EventLogConverter.java new file mode 100644 index 00000000..5948640c --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/metadata/EventLogConverter.java @@ -0,0 +1,86 @@ +/* + * 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.consumer.metadata; + +import io.knotx.fragments.engine.EventLogEntry; +import io.knotx.fragments.engine.EventLogEntry.NodeStatus; +import io.knotx.fragments.handler.LoggedNodeStatus; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +class EventLogConverter { + + private final List operationsLog; + + EventLogConverter(List operationsLog) { + this.operationsLog = operationsLog; + } + + NodeExecutionData getExecutionData(String id) { + List logs = getLogEntriesFor(id); + String transition = getTransition(logs); + + NodeExecutionData result = new NodeExecutionData(getLoggedNodesStatus(logs)); + if (transition != null) { + result.setResponse(transition, inJsonArray(getNodeLog(logs))); + } + return result; + } + + private LoggedNodeStatus getLoggedNodesStatus(List logs) { + return getLogForExecution(logs) + .map(LoggedNodeStatus::from) + .orElse(LoggedNodeStatus.UNPROCESSED); + } + + private String getTransition(List logs) { + return getLogForExecution(logs) + .map(EventLogEntry::getTransition) + .orElse(null); + } + + private JsonObject getNodeLog(List logs) { + return getLogForExecution(logs) + .map(EventLogEntry::getNodeLog) + .orElse(null); + } + + private List getLogEntriesFor(String id) { + return operationsLog.stream() + .filter(entry -> StringUtils.equals(id, entry.getNode())) + .collect(Collectors.toList()); + } + + private Optional getLogForExecution(List logs) { + return logs.stream() + .filter(log -> !NodeStatus.UNSUPPORTED_TRANSITION.equals(log.getStatus())) + .findFirst(); + } + + private JsonArray inJsonArray(JsonObject instance) { + if (instance != null) { + return new JsonArray().add(instance); + } else { + return new JsonArray(); + } + } + +} diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/consumer/metadata/MetadataConverter.java b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/metadata/MetadataConverter.java new file mode 100644 index 00000000..9ce91703 --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/metadata/MetadataConverter.java @@ -0,0 +1,121 @@ +/* + * 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.consumer.metadata; + +import static io.knotx.fragments.engine.api.node.single.FragmentResult.SUCCESS_TRANSITION; + +import io.knotx.fragments.engine.FragmentEvent; +import io.knotx.fragments.engine.NodeMetadata; +import io.knotx.fragments.engine.OperationMetadata; +import io.knotx.fragments.engine.TaskMetadata; +import io.knotx.fragments.engine.api.node.NodeType; +import io.knotx.fragments.handler.LoggedNodeStatus; +import io.knotx.fragments.handler.consumer.html.GraphNodeExecutionLog; +import io.knotx.fragments.handler.consumer.html.GraphNodeOperationLog; +import io.knotx.fragments.handler.consumer.html.GraphNodeResponseLog; +import io.knotx.fragments.handler.consumer.metadata.NodeExecutionData.Response; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +public class MetadataConverter { + + private final String rootNodeId; + private final Map nodes; + private final EventLogConverter eventLogConverter; + + public MetadataConverter(FragmentEvent event, TaskMetadata taskMetadata) { + this.rootNodeId = taskMetadata.getRootNodeId(); + this.nodes = taskMetadata.getNodesMetadata(); + this.eventLogConverter = new EventLogConverter(event.getLog().getOperations()); + } + + public GraphNodeExecutionLog getExecutionLog() { + return getExecutionLog(rootNodeId); + } + + private GraphNodeExecutionLog getExecutionLog(String nodeId) { + GraphNodeExecutionLog graphLog = fromMetadata(nodeId); + NodeExecutionData nodeExecutionData = eventLogConverter.getExecutionData(nodeId); + graphLog.setStatus(nodeExecutionData.getStatus()); + Response metadataResponse = nodeExecutionData.getResponse(); + if (metadataResponse != null) { + graphLog + .setResponse(GraphNodeResponseLog.newInstance(metadataResponse.getTransition(), + metadataResponse.getInvocations())); + } + if (containsUnsupportedTransitions(graphLog)) { + addMissingNode(graphLog); + } + return graphLog; + } + + private boolean containsUnsupportedTransitions(GraphNodeExecutionLog graphLog) { + String transition = graphLog.getResponse().getTransition(); + return transition != null + && !SUCCESS_TRANSITION.equals(transition) + && !graphLog.getOn().containsKey(transition); + } + + private void addMissingNode(GraphNodeExecutionLog graphLog) { + GraphNodeExecutionLog missingNode = GraphNodeExecutionLog + .newInstance(UUID.randomUUID().toString(), + NodeType.SINGLE, + "!", + Collections.emptyList(), + null, + Collections.emptyMap()) + .setStatus(LoggedNodeStatus.MISSING); + graphLog.getOn().put(graphLog.getResponse().getTransition(), missingNode); + } + + private GraphNodeExecutionLog fromMetadata(String id) { + if (nodes.containsKey(id)) { + NodeMetadata metadata = nodes.get(id); + return GraphNodeExecutionLog + .newInstance(metadata.getNodeId(), + metadata.getType(), + metadata.getLabel(), + getSubTasks(metadata.getNestedNodes()), + getOperationLog(metadata), + getTransitions(metadata.getTransitions())); + } else { + return GraphNodeExecutionLog.newInstance(id); + } + } + + private List getSubTasks(List nestedNodes) { + return nestedNodes.stream() + .map(this::getExecutionLog) + .collect(Collectors.toList()); + } + + private GraphNodeOperationLog getOperationLog(NodeMetadata metadata) { + OperationMetadata operationMetadata = metadata.getOperation(); + return GraphNodeOperationLog + .newInstance(operationMetadata.getFactory(), operationMetadata.getData()); + } + + private Map getTransitions( + Map definedTransitions) { + Map result = new HashMap<>(); + definedTransitions.forEach((name, nextId) -> result.put(name, getExecutionLog(nextId))); + return result; + } +} diff --git a/handler/core/src/main/java/io/knotx/fragments/handler/consumer/metadata/NodeExecutionData.java b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/metadata/NodeExecutionData.java new file mode 100644 index 00000000..3a2351cc --- /dev/null +++ b/handler/core/src/main/java/io/knotx/fragments/handler/consumer/metadata/NodeExecutionData.java @@ -0,0 +1,61 @@ +/* + * 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.consumer.metadata; + +import io.knotx.fragments.handler.LoggedNodeStatus; +import io.vertx.core.json.JsonArray; + +public class NodeExecutionData { + + private final LoggedNodeStatus status; + private Response response; + + NodeExecutionData(LoggedNodeStatus status) { + this.status = status; + } + + public LoggedNodeStatus getStatus() { + return status; + } + + public Response getResponse() { + return response; + } + + void setResponse(String transaction, JsonArray invocations) { + this.response = new Response(transaction, invocations); + } + + static class Response { + + private final String transition; + private final JsonArray invocations; + + Response(String transition, JsonArray invocations) { + this.transition = transition; + this.invocations = invocations; + } + + public String getTransition() { + return transition; + } + + public JsonArray getInvocations() { + return invocations; + } + } + +} 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 873cf535..a0f97c95 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 @@ -17,10 +17,14 @@ import io.knotx.fragments.engine.FragmentEventContext; import io.knotx.fragments.engine.api.Task; +import io.knotx.fragments.engine.TaskMetadata; +import io.knotx.fragments.engine.TaskWithMetadata; +import io.knotx.fragments.engine.api.node.Node; import io.knotx.fragments.handler.FragmentsHandlerOptions; import io.knotx.fragments.handler.api.exception.ConfigurationException; import io.vertx.core.json.JsonObject; import io.vertx.reactivex.core.Vertx; +import org.apache.commons.lang3.StringUtils; /** * A task factory interface allowing to register a task factory by its name. Implementing class must @@ -58,8 +62,34 @@ public interface TaskFactory { * returns true. When called with a fragment that does not provide a task name, then * {@link ConfigurationException} is thrown. * + * Attempts to fill TaskMetadata with information on task structure. + * * @param context fragment event context * @return new task instance + * @deprecated use {@link #newInstanceWithMetadata(FragmentEventContext)} instead */ + @Deprecated Task newInstance(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 ConfigurationException} is thrown. + * + * Attempts to fill TaskMetadata with information on task structure. + * + * @param context fragment event context + * @return new task instance with metadata + */ + default TaskWithMetadata newInstanceWithMetadata(FragmentEventContext context) { + Task task = newInstance(context); + return new TaskWithMetadata( + task, + TaskMetadata.noMetadata( + task.getName(), + task.getRootNode().map(Node::getId).orElse(StringUtils.EMPTY) + ) + ); + } + } 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 index 09b738e3..6e1c5ecb 100644 --- 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 @@ -15,11 +15,14 @@ */ package io.knotx.fragments.task.factory; -import io.knotx.fragments.handler.api.exception.ConfigurationException; import io.knotx.fragments.api.Fragment; import io.knotx.fragments.engine.FragmentEventContext; +import io.knotx.fragments.engine.NodeMetadata; +import io.knotx.fragments.engine.TaskMetadata; +import io.knotx.fragments.engine.TaskWithMetadata; import io.knotx.fragments.engine.api.Task; import io.knotx.fragments.engine.api.node.Node; +import io.knotx.fragments.handler.api.exception.ConfigurationException; import io.knotx.fragments.task.TaskFactory; import io.knotx.fragments.task.exception.NodeFactoryNotFoundException; import io.knotx.fragments.task.factory.node.NodeFactory; @@ -37,7 +40,7 @@ public class DefaultTaskFactory implements TaskFactory, NodeProvider { - public static final String NAME = "default"; + private static final String NAME = "default"; private DefaultTaskFactoryConfig taskFactoryConfig; private Map nodeFactories; @@ -68,7 +71,13 @@ private boolean isTaskConfigured(Fragment fragment) { } @Override - public Task newInstance(FragmentEventContext eventContext) { + public Task newInstance(FragmentEventContext context) { + // The implementation is for backwards compatibility of NodeFactory interface + return newInstanceWithMetadata(context).getTask(); + } + + @Override + public TaskWithMetadata newInstanceWithMetadata(FragmentEventContext eventContext) { Fragment fragment = eventContext.getFragmentEvent().getFragment(); String taskKey = taskFactoryConfig.getTaskNameKey(); String taskName = fragment.getConfiguration().getString(taskKey); @@ -76,16 +85,24 @@ public Task newInstance(FragmentEventContext eventContext) { Map tasks = taskFactoryConfig.getTasks(); return Optional.ofNullable(tasks.get(taskName)) .map(rootGraphNodeOptions -> { - Node rootNode = initNode(rootGraphNodeOptions); - return new Task(taskName, rootNode); + Map nodesMetadata = new HashMap<>(); + Node rootNode = initNode(rootGraphNodeOptions, nodesMetadata); + return new TaskWithMetadata(new Task(taskName, rootNode), + TaskMetadata.create(taskName, rootNode.getId(), nodesMetadata)); }) .orElseThrow(() -> new ConfigurationException("Task [" + taskName + "] not configured!")); } @Override public Node initNode(GraphNodeOptions nodeOptions) { + return initNode(nodeOptions, new HashMap<>()); + } + + @Override + public Node initNode(GraphNodeOptions nodeOptions, Map nodesMetadata) { return findNodeFactory(nodeOptions) - .map(f -> f.initNode(nodeOptions, initTransitions(nodeOptions), this)) + .map(f -> f.initNode(nodeOptions.getNode(), initTransitions(nodeOptions, nodesMetadata), this, + nodesMetadata)) .orElseThrow(() -> new NodeFactoryNotFoundException(nodeOptions.getNode().getFactory())); } @@ -93,11 +110,12 @@ private Optional findNodeFactory(GraphNodeOptions nodeOptions) { return Optional.ofNullable(nodeFactories.get(nodeOptions.getNode().getFactory())); } - private Map initTransitions(GraphNodeOptions nodeOptions) { + private Map initTransitions(GraphNodeOptions nodeOptions, + Map nodesMetadata) { Map transitions = nodeOptions.getOnTransitions(); Map edges = new HashMap<>(); transitions.forEach((transition, childGraphOptions) -> edges - .put(transition, initNode(childGraphOptions))); + .put(transition, initNode(childGraphOptions, nodesMetadata))); return edges; } diff --git a/handler/core/src/main/java/io/knotx/fragments/task/factory/NodeProvider.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/NodeProvider.java index 21635502..c6e68d34 100644 --- a/handler/core/src/main/java/io/knotx/fragments/task/factory/NodeProvider.java +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/NodeProvider.java @@ -15,13 +15,40 @@ */ package io.knotx.fragments.task.factory; +import io.knotx.fragments.engine.NodeMetadata; import io.knotx.fragments.engine.api.node.Node; +import io.knotx.fragments.task.factory.node.NodeOptions; +import java.util.Map; /** - * Inits node based on node options. + * Initialize {@link io.knotx.fragments.engine.api.node.single.SingleNode} or {@link + * io.knotx.fragments.engine.api.node.composite.CompositeNode} instances based on node options. */ public interface NodeProvider { + /** + * Init a graph node based on provided options. If factory defined in {@link + * NodeOptions#getFactory()} is not found, then it throws {@link io.knotx.fragments.task.exception.NodeFactoryNotFoundException}. + * + * @param nodeOptions node options + * @return {@link io.knotx.fragments.engine.api.node.single.SingleNode} or {@link + * io.knotx.fragments.engine.api.node.composite.CompositeNode} instance + * @deprecated use {@link #initNode(GraphNodeOptions, Map)} instead + */ + @Deprecated Node initNode(GraphNodeOptions nodeOptions); + /** + * Init a graph node based on provided options. If factory defined in {@link + * NodeOptions#getFactory()} is not found, then it throws {@link io.knotx.fragments.task.exception.NodeFactoryNotFoundException}. + * Additionally it adds node's metadata to nodesMetadata map. + * + * @param nodeOptions node options + * @return {@link io.knotx.fragments.engine.api.node.single.SingleNode} or {@link + * io.knotx.fragments.engine.api.node.composite.CompositeNode} instance + */ + default Node initNode(GraphNodeOptions nodeOptions, Map nodesMetadata) { + return initNode(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 index 37d52a05..7f63e12f 100644 --- 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 @@ -15,6 +15,9 @@ */ package io.knotx.fragments.task.factory.node; +import static java.util.Collections.emptyMap; + +import io.knotx.fragments.engine.NodeMetadata; import io.knotx.fragments.engine.api.node.Node; import io.knotx.fragments.task.factory.GraphNodeOptions; import io.knotx.fragments.task.factory.NodeProvider; @@ -54,7 +57,22 @@ public interface NodeFactory { * @param edges - prepared node outgoing edges * @param nodeProvider - node provider if the current node contains others * @return node instance + * @deprecated use {@link #initNode(NodeOptions, Map, NodeProvider, Map)} instead. */ + @Deprecated Node initNode(GraphNodeOptions nodeOptions, Map edges, NodeProvider nodeProvider); + /** + * 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 + * @param nodesMetadata - node id to metadata map + * @return node instance + */ + default Node initNode(NodeOptions nodeOptions, Map edges, NodeProvider nodeProvider, + Map nodesMetadata) { + return initNode(new GraphNodeOptions(nodeOptions, emptyMap()), edges, nodeProvider); + } } 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 index d7ff8f50..de0e3ccc 100644 --- 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 @@ -15,29 +15,41 @@ */ package io.knotx.fragments.task.factory.node.action; +import static io.knotx.fragments.engine.NodeMetadata.single; + +import io.knotx.fragments.engine.NodeMetadata; +import io.knotx.fragments.engine.OperationMetadata; import io.knotx.fragments.engine.api.node.Node; +import io.knotx.fragments.engine.api.node.single.FragmentContext; +import io.knotx.fragments.engine.api.node.single.FragmentResult; import io.knotx.fragments.engine.api.node.single.SingleNode; import io.knotx.fragments.handler.api.Action; import io.knotx.fragments.handler.api.ActionFactory; -import io.knotx.fragments.engine.api.node.single.FragmentContext; -import io.knotx.fragments.engine.api.node.single.FragmentResult; +import io.knotx.fragments.task.factory.ActionFactoryOptions; +import io.knotx.fragments.task.factory.GraphNodeOptions; import io.knotx.fragments.task.factory.NodeProvider; import io.knotx.fragments.task.factory.node.NodeFactory; -import io.knotx.fragments.task.factory.GraphNodeOptions; +import io.knotx.fragments.task.factory.node.NodeOptions; import io.reactivex.Single; import io.vertx.core.json.JsonObject; import io.vertx.reactivex.core.Vertx; +import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Optional; import java.util.ServiceLoader; +import java.util.UUID; import java.util.function.Function; import java.util.function.Supplier; public class ActionNodeFactory implements NodeFactory { public static final String NAME = "action"; + public static final String METADATA_ALIAS = "alias"; + public static final String METADATA_ACTION_FACTORY = "actionFactory"; + public static final String METADATA_ACTION_CONFIG = "actionConfig"; private ActionProvider actionProvider; + private Map actionNameToOptions; @Override public String getName() { @@ -46,21 +58,30 @@ public String getName() { @Override public ActionNodeFactory configure(JsonObject config, Vertx vertx) { - actionProvider = new ActionProvider(supplyFactories(), - new ActionNodeFactoryConfig(config).getActions(), vertx); + this.actionNameToOptions = new ActionNodeFactoryConfig(config).getActions(); + actionProvider = new ActionProvider(supplyFactories(), actionNameToOptions, vertx); return this; } @Override public Node initNode(GraphNodeOptions nodeOptions, Map edges, NodeProvider nodeProvider) { - ActionNodeConfig config = new ActionNodeConfig(nodeOptions.getNode().getConfig()); + // The implementation is for backwards compatibility of NodeFactory interface + return initNode(nodeOptions.getNode(), edges, nodeProvider, new HashMap<>()); + } + + @Override + public Node initNode(NodeOptions nodeOptions, Map edges, NodeProvider nodeProvider, + Map nodesMetadata) { + ActionNodeConfig config = new ActionNodeConfig(nodeOptions.getConfig()); Action action = actionProvider.get(config.getAction()).orElseThrow( () -> new ActionNotFoundException(config.getAction())); + final String actionNodeId = UUID.randomUUID().toString(); + nodesMetadata.put(actionNodeId, createActionNodeMetadata(actionNodeId, edges, config)); return new SingleNode() { @Override public String getId() { - return config.getAction(); + return actionNodeId; } @Override @@ -75,6 +96,26 @@ public Single execute(FragmentContext fragmentContext) { }; } + private NodeMetadata createActionNodeMetadata(String actionNodeId, Map edges, + ActionNodeConfig config) { + Map transitionMetadata = createTransitionMetadata(edges); + return single(actionNodeId, config.getAction(), transitionMetadata, createOperation(config)); + } + + private Map createTransitionMetadata(Map edges) { + Map transitionMetadata = new HashMap<>(); + edges.forEach((transition, node) -> transitionMetadata.put(transition, node.getId())); + return transitionMetadata; + } + + private OperationMetadata createOperation(ActionNodeConfig config) { + ActionFactoryOptions actionConfig = actionNameToOptions.get(config.getAction()); + return new OperationMetadata(NAME, new JsonObject() + .put(METADATA_ALIAS, config.getAction()) + .put(METADATA_ACTION_FACTORY, actionConfig.getFactory()) + .put(METADATA_ACTION_CONFIG, actionConfig.getConfig())); + } + private Function> toRxFunction( Action action) { io.knotx.fragments.handler.reactivex.api.Action rxAction = io.knotx.fragments.handler.reactivex.api.Action 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 index a998fe47..4b817888 100644 --- 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 @@ -15,6 +15,7 @@ */ package io.knotx.fragments.task.factory.node.action; +import io.knotx.fragments.handler.api.actionlog.ActionLogLevel; import io.knotx.fragments.task.factory.ActionFactoryOptions; import io.knotx.fragments.task.factory.LogLevelConfig; import io.vertx.codegen.annotations.DataObject; @@ -32,20 +33,25 @@ public class ActionNodeFactoryConfig { private Map actions; public ActionNodeFactoryConfig(Map actions) { + this(actions, ActionLogLevel.fromConfig(new LogLevelConfig().toJson())); + } + + public ActionNodeFactoryConfig(Map actions, ActionLogLevel globalLogLevel) { this.actions = actions; + initActionLogLevel(globalLogLevel.getLevel()); } public ActionNodeFactoryConfig(JsonObject json) { actions = new HashMap<>(); ActionNodeFactoryConfigConverter.fromJson(json, this); - initActionLogLevel(json); + LogLevelConfig globalLogLevel = new LogLevelConfig(json); + initActionLogLevel(globalLogLevel.getLogLevel()); } - private void initActionLogLevel(JsonObject json) { - LogLevelConfig globalLogLevel = new LogLevelConfig(json); + private void initActionLogLevel(String logLevel) { actions.values().forEach(actionOptions -> { JsonObject actionConfig = actionOptions.getConfig(); - LogLevelConfig.override(actionConfig, globalLogLevel.getLogLevel()); + LogLevelConfig.override(actionConfig, logLevel); }); } diff --git a/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionProvider.java b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionProvider.java index f1e37d52..edb7297d 100644 --- a/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionProvider.java +++ b/handler/core/src/main/java/io/knotx/fragments/task/factory/node/action/ActionProvider.java @@ -15,10 +15,11 @@ */ package io.knotx.fragments.task.factory.node.action; -import io.knotx.fragments.task.factory.ActionFactoryOptions; + 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.task.factory.ActionFactoryOptions; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.reactivex.core.Vertx; 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 index 8f18eb35..860b94ba 100644 --- 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 @@ -15,26 +15,31 @@ */ package io.knotx.fragments.task.factory.node.subtasks; +import static io.knotx.fragments.engine.NodeMetadata.composite; import static io.knotx.fragments.engine.api.node.single.FragmentResult.ERROR_TRANSITION; import static io.knotx.fragments.engine.api.node.single.FragmentResult.SUCCESS_TRANSITION; -import io.knotx.fragments.engine.api.node.composite.CompositeNode; +import io.knotx.fragments.engine.NodeMetadata; +import io.knotx.fragments.engine.OperationMetadata; import io.knotx.fragments.engine.api.node.Node; +import io.knotx.fragments.engine.api.node.composite.CompositeNode; +import io.knotx.fragments.task.factory.GraphNodeOptions; import io.knotx.fragments.task.factory.NodeProvider; import io.knotx.fragments.task.factory.node.NodeFactory; -import io.knotx.fragments.task.factory.GraphNodeOptions; +import io.knotx.fragments.task.factory.node.NodeOptions; import io.vertx.core.json.JsonObject; import io.vertx.reactivex.core.Vertx; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; public class SubtasksNodeFactory implements NodeFactory { public static final String NAME = "subtasks"; - public static final String COMPOSITE_NODE_ID = "composite"; @Override public String getName() { @@ -50,14 +55,22 @@ public SubtasksNodeFactory configure(JsonObject config, Vertx vertx) { @Override public Node initNode(GraphNodeOptions nodeOptions, Map edges, NodeProvider nodeProvider) { - SubtasksNodeConfig config = new SubtasksNodeConfig(nodeOptions.getNode().getConfig()); + // The implementation is for backwards compatibility of NodeFactory interface + return initNode(nodeOptions.getNode(), edges, nodeProvider, new HashMap<>()); + } + + @Override + public Node initNode(NodeOptions nodeOptions, Map edges, NodeProvider nodeProvider, Map nodesMetadata) { + SubtasksNodeConfig config = new SubtasksNodeConfig(nodeOptions.getConfig()); List nodes = config.getSubtasks().stream() - .map(nodeProvider::initNode) + .map(subTaskConfig -> nodeProvider.initNode(subTaskConfig, nodesMetadata)) .collect(Collectors.toList()); + final String nodeId = UUID.randomUUID().toString(); + nodesMetadata.put(nodeId, createSubTaskNodeMetadata(nodeId, edges, nodes)); return new CompositeNode() { @Override public String getId() { - return getNodeId(); + return nodeId; } @Override @@ -77,8 +90,18 @@ private Optional filter(String transition) { }; } - private String getNodeId() { - // TODO /~https://github.com/Knotx/knotx-fragments/issues/54 - return COMPOSITE_NODE_ID; + private NodeMetadata createSubTaskNodeMetadata(String nodeId, Map edges, + List nodes) { + List nestedNodesIds = nodes.stream().map(Node::getId).collect(Collectors.toList()); + Map transitionMetadata = createTransitionMetadata(edges); + return composite(nodeId, "composite", transitionMetadata, nestedNodesIds, + new OperationMetadata(NAME)); } + + private Map createTransitionMetadata(Map edges) { + Map transitionMetadata = new HashMap<>(); + edges.forEach((transition, node) -> transitionMetadata.put(transition, node.getId())); + return transitionMetadata; + } + } diff --git a/handler/core/src/main/resources/META-INF/services/io.knotx.fragments.handler.consumer.FragmentEventsConsumerFactory b/handler/core/src/main/resources/META-INF/services/io.knotx.fragments.handler.consumer.FragmentEventsConsumerFactory index 36993be8..90b21918 100644 --- a/handler/core/src/main/resources/META-INF/services/io.knotx.fragments.handler.consumer.FragmentEventsConsumerFactory +++ b/handler/core/src/main/resources/META-INF/services/io.knotx.fragments.handler.consumer.FragmentEventsConsumerFactory @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -io.knotx.fragments.handler.consumer.FragmentHtmlBodyWriterFactory \ No newline at end of file +io.knotx.fragments.handler.consumer.html.FragmentHtmlBodyWriterFactory \ No newline at end of file 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 69df2181..b0eee443 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 @@ -37,9 +37,11 @@ import io.vertx.junit5.VertxTestContext; import io.vertx.reactivex.core.Vertx; import io.vertx.reactivex.ext.web.RoutingContext; + import java.util.Collections; import java.util.List; import java.util.Optional; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -83,20 +85,21 @@ void shouldSuccess(Vertx vertx, VertxTestContext testContext) void shouldFail(Vertx vertx, VertxTestContext testContext) throws Throwable { HoconLoader.verify("handler/singleTaskFactoryWithFailingTask.conf", config -> { - //given + // given RoutingContext routingContext = mockRoutingContext("failing-task"); FragmentsHandler underTest = new FragmentsHandler(vertx, config); - //when - underTest.handle(routingContext); - - //then + // when doAnswer(invocation -> { testContext.completeNow(); return null; }) .when(routingContext) .fail(500); + + underTest.handle(routingContext); + + // then verified as correct (assertion inside HoconLoader::verify) }, testContext, vertx); } @@ -111,8 +114,9 @@ void snippetFragmentWithHtmlConsumer(Vertx vertx, VertxTestContext testContext) EMPTY_BODY); // when - Single> rxDoHandle = new FragmentsHandler(vertx, config) - .doHandle(Collections.singletonList(fragment), new ClientRequest()); + FragmentsHandler handler = new FragmentsHandler(vertx, config); + Single> rxDoHandle = handler + .doHandle(handler.createExecutionPlan(Collections.singletonList(fragment), new ClientRequest())); rxDoHandle.subscribe( result -> testContext.verify(() -> { @@ -142,7 +146,7 @@ void taskFactoryWithTaskEndingWithSuccess(Vertx vertx, VertxTestContext testCont //when Single> rxDoHandle = underTest - .doHandle(Collections.singletonList(fragment), new ClientRequest()); + .doHandle(underTest.createExecutionPlan(Collections.singletonList(fragment), new ClientRequest())); rxDoHandle.subscribe( result -> testContext.verify(() -> { @@ -168,7 +172,7 @@ void singleFactoryNotAcceptingFragment(Vertx vertx, VertxTestContext testContext //when Single> rxDoHandle = underTest - .doHandle(Collections.singletonList(fragment), new ClientRequest()); + .doHandle(underTest.createExecutionPlan(Collections.singletonList(fragment), new ClientRequest())); rxDoHandle.subscribe( result -> testContext.verify(() -> { @@ -194,7 +198,7 @@ void twoFactoriesWithTheSameName(Vertx vertx, VertxTestContext testContext) //when Single> rxDoHandle = underTest - .doHandle(Collections.singletonList(fragment), new ClientRequest()); + .doHandle(underTest.createExecutionPlan(Collections.singletonList(fragment), new ClientRequest())); rxDoHandle.subscribe( result -> testContext.verify(() -> { @@ -283,8 +287,7 @@ private RoutingContext mockRoutingContext(String task) { RoutingContext routingContext = Mockito.mock(RoutingContext.class); when(routingContext.get(eq(RequestContext.KEY))).thenReturn(requestContext); - when(routingContext.get(eq("fragments"))).thenReturn( - newArrayList(fragment(task))); + when(routingContext.get(eq("fragments"))).thenReturn(newArrayList(fragment(task))); return routingContext; } diff --git a/handler/core/src/test/java/io/knotx/fragments/handler/LoggedNodeStatusTest.java b/handler/core/src/test/java/io/knotx/fragments/handler/LoggedNodeStatusTest.java new file mode 100644 index 00000000..8060d186 --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/handler/LoggedNodeStatusTest.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.handler; + +import io.knotx.fragments.api.Fragment; +import io.knotx.fragments.engine.EventLogEntry; +import io.knotx.fragments.engine.api.node.single.FragmentResult; +import io.vertx.core.json.JsonObject; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class LoggedNodeStatusTest { + + private static final String TASK_NAME = "some-task"; + private static final String ROOT_NODE = "1234-4321-1234"; + + @Test + @DisplayName("Entry with success transition matches SUCCESS status") + void entryWithSuccessTransitionMatchesSuccessStatus() { + // given + EventLogEntry entry = EventLogEntry.success(TASK_NAME, ROOT_NODE, createFragmentResult("_success")); + + // when + LoggedNodeStatus status = LoggedNodeStatus.from(entry); + + // then + assertEquals(LoggedNodeStatus.SUCCESS, status); + } + + @Test + @DisplayName("Entry with error transition matches ERROR status") + void entryWithErrorTransitionMatchesSuccessStatus() { + // given + EventLogEntry entry = EventLogEntry.error(TASK_NAME, ROOT_NODE, "_error"); + + // when + LoggedNodeStatus status = LoggedNodeStatus.from(entry); + + // then + assertEquals(LoggedNodeStatus.ERROR, status); + } + + @Test + @DisplayName("Timed-out entry matches ERROR status") + void timedOutEntryMatchesErrorStatus() { + // given + EventLogEntry entry = EventLogEntry.timeout(TASK_NAME, ROOT_NODE); + + // when + LoggedNodeStatus status = LoggedNodeStatus.from(entry); + + // then + assertEquals(LoggedNodeStatus.ERROR, status); + } + + @Test + @DisplayName("Entry with custom transition matches OTHER status") + void entryWithCustomTransitionMatchesOtherStatus() { + // given + EventLogEntry entry = EventLogEntry.success(TASK_NAME, ROOT_NODE, createFragmentResult("custom")); + + // when + LoggedNodeStatus status = LoggedNodeStatus.from(entry); + + // then + assertEquals(LoggedNodeStatus.OTHER, status); + } + + @Test + @DisplayName("Unprocessed entry matches UNPROCESSED status") + void unprocessedEntryMatchesUnprocessedStatus() { + // given + EventLogEntry entry = EventLogEntry.unprocessed(TASK_NAME, ROOT_NODE); + + // when + LoggedNodeStatus status = LoggedNodeStatus.from(entry); + + // then + assertEquals(LoggedNodeStatus.UNPROCESSED, status); + } + + @Test + @DisplayName("Entry with unsupported transition status has no match and throws exception") + void entryWithUnsupportedTransitionHasNoMatchAndThrowsException() { + // given + EventLogEntry entry = EventLogEntry.unsupported(TASK_NAME, ROOT_NODE, "unknown"); + + // when + Executable matching = () -> LoggedNodeStatus.from(entry); + + // then + assertThrows(IllegalArgumentException.class, matching); + } + + private FragmentResult createFragmentResult(String transition) { + return new FragmentResult(new Fragment("_empty", new JsonObject(), ""), transition, new JsonObject()); + } +} \ No newline at end of file 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 fe015125..052ac866 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 @@ -34,7 +34,6 @@ public String getName() { @Override public Action create(String alias, JsonObject config, Vertx vertx, Action doAction) { - String transition = config.getString("transition"); return (fragmentContext, resultHandler) -> { diff --git a/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/FragmentExecutionLogTest.java b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/FragmentExecutionLogTest.java new file mode 100644 index 00000000..76f94317 --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/FragmentExecutionLogTest.java @@ -0,0 +1,70 @@ +/* + * 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.consumer.html; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import io.knotx.fragments.api.Fragment; +import io.knotx.fragments.engine.FragmentEvent; +import io.knotx.fragments.engine.FragmentEvent.Status; +import io.vertx.core.json.JsonObject; +import org.junit.jupiter.api.Test; + +class FragmentExecutionLogTest { + + @Test + void validateSerialization() { + // given + Fragment fragment = new Fragment("snippet", new JsonObject(), "body"); + Status eventStatus = Status.SUCCESS; + FragmentEvent fragmentEvent = new FragmentEvent(fragment).setStatus(eventStatus); + + FragmentExecutionLog executionLog = FragmentExecutionLog.newInstance(fragmentEvent); + + // when + FragmentExecutionLog result = new FragmentExecutionLog(executionLog.toJson()); + + // then + assertEquals(eventStatus, result.getStatus()); + assertEquals(fragment, result.getFragment()); + assertEquals(0, result.getStartTime()); + assertEquals(0, result.getFinishTime()); + assertNull(result.getGraph()); + } + + @Test + void validateSerializationWithGraph() { + // given + Fragment fragment = new Fragment("snippet", new JsonObject(), "body"); + Status eventStatus = Status.SUCCESS; + FragmentEvent fragmentEvent = new FragmentEvent(fragment).setStatus(eventStatus); + GraphNodeExecutionLog graphLog = GraphNodeExecutionLog.newInstance("id"); + + FragmentExecutionLog executionLog = FragmentExecutionLog.newInstance(fragmentEvent, graphLog); + + // when + FragmentExecutionLog result = new FragmentExecutionLog(executionLog.toJson()); + + // then + assertEquals(eventStatus, result.getStatus()); + assertEquals(fragment, result.getFragment()); + assertEquals(0, result.getStartTime()); + assertEquals(0, result.getFinishTime()); + assertEquals(graphLog, result.getGraph()); + } + +} \ No newline at end of file diff --git a/handler/core/src/test/java/io/knotx/fragments/handler/consumer/FragmentHtmlBodyWriterFactoryTest.java b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/FragmentHtmlBodyWriterFactoryTest.java similarity index 63% rename from handler/core/src/test/java/io/knotx/fragments/handler/consumer/FragmentHtmlBodyWriterFactoryTest.java rename to handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/FragmentHtmlBodyWriterFactoryTest.java index 69a9c7f0..2ad5551d 100644 --- a/handler/core/src/test/java/io/knotx/fragments/handler/consumer/FragmentHtmlBodyWriterFactoryTest.java +++ b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/FragmentHtmlBodyWriterFactoryTest.java @@ -13,22 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.knotx.fragments.handler.consumer; +package io.knotx.fragments.handler.consumer.html; -import static io.knotx.fragments.handler.consumer.FragmentHtmlBodyWriterFactory.CONDITION_OPTION; -import static io.knotx.fragments.handler.consumer.FragmentHtmlBodyWriterFactory.FRAGMENT_TYPES_OPTIONS; -import static io.knotx.fragments.handler.consumer.FragmentHtmlBodyWriterFactory.HEADER_OPTION; +import static io.knotx.fragments.handler.consumer.html.FragmentHtmlBodyWriterFactory.CONDITION_OPTION; +import static io.knotx.fragments.handler.consumer.html.FragmentHtmlBodyWriterFactory.FRAGMENT_TYPES_OPTIONS; +import static io.knotx.fragments.handler.consumer.html.FragmentHtmlBodyWriterFactory.HEADER_OPTION; +import static io.knotx.junit5.assertions.KnotxAssertions.assertJsonEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import io.knotx.fragments.api.Fragment; +import io.knotx.fragments.engine.EventLogEntry.NodeStatus; import io.knotx.fragments.engine.FragmentEvent; +import io.knotx.fragments.engine.TaskMetadata; +import io.knotx.fragments.engine.TasksMetadata; +import io.knotx.fragments.handler.consumer.FragmentEventsConsumer; import io.knotx.server.api.context.ClientRequest; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.reactivex.core.MultiMap; +import java.util.Collections; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.jupiter.api.DisplayName; @@ -36,9 +43,9 @@ class FragmentHtmlBodyWriterFactoryTest { - public static final String EXPECTED_FRAGMENT_TYPE = "snippet"; - public static final String EXPECTED_HEADER = "x-knotx-debug"; - public static final String EXPECTED_PARAM = "debug"; + private static final String EXPECTED_FRAGMENT_TYPE = "snippet"; + private static final String EXPECTED_HEADER = "x-knotx-debug"; + private static final String EXPECTED_PARAM = "debug"; private static final String PARAM_OPTION = "param"; @Test @@ -52,9 +59,9 @@ void expectFragmentNotModifiedWhenConditionNotConfigured() { // when FragmentEventsConsumer tested = new FragmentHtmlBodyWriterFactory() - .create(new JsonObject().put(FRAGMENT_TYPES_OPTIONS, new JsonArray().add( - EXPECTED_FRAGMENT_TYPE))); - tested.accept(new ClientRequest(), ImmutableList.of(original)); + .create(new JsonObject() + .put(FRAGMENT_TYPES_OPTIONS, new JsonArray().add(EXPECTED_FRAGMENT_TYPE))); + tested.accept(new ClientRequest(), ImmutableList.of(original), emptyTasksMetadata()); // then assertEquals(copy, original); @@ -75,7 +82,7 @@ void expectFragmentNotModifiedWhenSupportedTypesNotConfigured() { .put(CONDITION_OPTION, new JsonObject().put(HEADER_OPTION, EXPECTED_HEADER))); tested.accept(new ClientRequest() .setHeaders(MultiMap.caseInsensitiveMultiMap().add(EXPECTED_HEADER, "true")), - ImmutableList.of(original)); + ImmutableList.of(original), emptyTasksMetadata()); // then assertEquals(copy, original); @@ -95,7 +102,7 @@ void expectFragmentNotModifiedWhenOtherSupportedTypeConfigured() { .put(CONDITION_OPTION, new JsonObject().put(HEADER_OPTION, EXPECTED_HEADER))); tested.accept(new ClientRequest() .setHeaders(MultiMap.caseInsensitiveMultiMap().add(EXPECTED_HEADER, "true")), - ImmutableList.of(original)); + ImmutableList.of(original), emptyTasksMetadata()); // then assertEquals(copy, original); @@ -116,9 +123,8 @@ void expectFragmentBodyModifiedWhenHeaderConditionConfigured() { .put(CONDITION_OPTION, new JsonObject().put(HEADER_OPTION, EXPECTED_HEADER))); tested.accept(new ClientRequest() .setHeaders(MultiMap.caseInsensitiveMultiMap().add(EXPECTED_HEADER, "true")), - ImmutableList.of(original)); + ImmutableList.of(original), emptyTasksMetadata()); - // then // then assertNotEquals(copy, original); } @@ -138,9 +144,8 @@ void expectFragmentBodyModifiedWhenParamConditionConfigured() { .put(CONDITION_OPTION, new JsonObject().put(PARAM_OPTION, EXPECTED_PARAM))); tested.accept(new ClientRequest() .setParams(MultiMap.caseInsensitiveMultiMap().add(EXPECTED_PARAM, "true")), - ImmutableList.of(original)); + ImmutableList.of(original), emptyTasksMetadata()); - // then // then assertNotEquals(copy, original); } @@ -158,7 +163,7 @@ void expectFragmentBodyWrappedByFragmentId() { .put(CONDITION_OPTION, new JsonObject().put(PARAM_OPTION, EXPECTED_PARAM))); tested.accept(new ClientRequest() .setParams(MultiMap.caseInsensitiveMultiMap().add(EXPECTED_PARAM, "true")), - ImmutableList.of(event)); + ImmutableList.of(event), emptyTasksMetadata()); // then assertTrue(event.getFragment().getBody() @@ -172,11 +177,21 @@ void expectFragmentBodyWrappedByFragmentId() { void expectFragmentBodyContainsDebugScript() { //given String body = "
body
"; - FragmentEvent event = new FragmentEvent(new Fragment("snippet", new JsonObject(), body)); - JsonObject eventData = event.toJson(); + Fragment fragment = new Fragment("snippet", new JsonObject(), body); + FragmentEvent event = new FragmentEvent(fragment); - String scriptRegexp = ""; + JsonObject expectedLog = new JsonObject() + .put("startTime", 0) + .put("finishTime", 0) + .put("status", "UNPROCESSED") + .put("fragment", new JsonObject() + .put("id", fragment.getId()) + .put("type", "snippet")) + .put("graph", new JsonObject()); + + String scriptRegexp = + ""; Pattern scriptPattern = Pattern.compile(scriptRegexp, Pattern.DOTALL); // when @@ -185,17 +200,17 @@ void expectFragmentBodyContainsDebugScript() { .put(CONDITION_OPTION, new JsonObject().put(PARAM_OPTION, EXPECTED_PARAM))); tested.accept(new ClientRequest() .setParams(MultiMap.caseInsensitiveMultiMap().add(EXPECTED_PARAM, "true")), - ImmutableList.of(event)); + ImmutableList.of(event), emptyTasksMetadata()); // then Matcher matcher = scriptPattern.matcher(event.getFragment().getBody()); assertTrue(matcher.find()); - assertEquals(eventData, new JsonObject(matcher.group("fragmentEventJson"))); + assertJsonEquals(expectedLog, new JsonObject(matcher.group("fragmentEventJson"))); } @Test - @DisplayName("Expect debug script is first HTML tag.") - void expectDebugScriptAfterComment() { + @DisplayName("Expect log debug script is a first HTML tag.") + void expectLogDebugScriptAfterComment() { //given String body = "
body
"; FragmentEvent event = new FragmentEvent(new Fragment("snippet", new JsonObject(), body)); @@ -206,14 +221,62 @@ void expectDebugScriptAfterComment() { .put(CONDITION_OPTION, new JsonObject().put(PARAM_OPTION, EXPECTED_PARAM))); tested.accept(new ClientRequest() .setParams(MultiMap.caseInsensitiveMultiMap().add(EXPECTED_PARAM, "true")), - ImmutableList.of(event)); + ImmutableList.of(event), emptyTasksMetadata()); // then String bodyWithoutComments = event.getFragment().getBody() .replaceAll("", ""); assertTrue( - bodyWithoutComments.startsWith(".*", Pattern.DOTALL); + + Matcher matcher = secondTagsContent.matcher(bodyWithoutComments); + + assertTrue(matcher.matches()); + JsonObject output = new JsonObject(matcher.group(1)); + assertJsonEquals(expectedLog, output); } -} \ No newline at end of file + private TasksMetadata emptyTasksMetadata() { + return new TasksMetadata(Collections.emptyMap()); + } +} diff --git a/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/GraphNodeExecutionLogTest.java b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/GraphNodeExecutionLogTest.java new file mode 100644 index 00000000..bacb2c4d --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/GraphNodeExecutionLogTest.java @@ -0,0 +1,78 @@ +/* + * 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.consumer.html; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.knotx.fragments.engine.api.node.NodeType; +import io.knotx.fragments.engine.api.node.single.FragmentResult; +import io.vertx.core.json.JsonObject; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class GraphNodeExecutionLogTest { + + @Test + void validateDefaults() { + // given + String nodeId = "A"; + + GraphNodeExecutionLog executionLog = GraphNodeExecutionLog.newInstance(nodeId); + + // when + GraphNodeExecutionLog result = new GraphNodeExecutionLog(executionLog.toJson()); + + // then + assertEquals(nodeId, result.getId()); + assertEquals(NodeType.SINGLE, result.getType()); + assertTrue(result.getLabel().isEmpty()); + assertTrue(result.getSubtasks().isEmpty()); + assertTrue(result.getOn().isEmpty()); + } + + @Test + void validateSerialization() { + // given + String nodeId = "A"; + NodeType type = NodeType.COMPOSITE; + String label = "A Label"; + + List subtasks = Collections + .singletonList(GraphNodeExecutionLog.newInstance("AA")); + GraphNodeOperationLog operation = GraphNodeOperationLog + .newInstance("factory", new JsonObject()); + Map on = Collections + .singletonMap(FragmentResult.SUCCESS_TRANSITION, GraphNodeExecutionLog.newInstance("B")); + + GraphNodeExecutionLog executionLog = GraphNodeExecutionLog + .newInstance(nodeId, type, label, subtasks, operation, on); + + // when + GraphNodeExecutionLog result = new GraphNodeExecutionLog(executionLog.toJson()); + + // then + assertEquals(nodeId, result.getId()); + assertEquals(type, result.getType()); + assertEquals(label, result.getLabel()); + assertEquals(subtasks, result.getSubtasks()); + assertEquals(operation, result.getOperation()); + assertEquals(on, result.getOn()); + } + +} \ No newline at end of file diff --git a/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/GraphNodeOperationLogTest.java b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/GraphNodeOperationLogTest.java new file mode 100644 index 00000000..5b78cb34 --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/GraphNodeOperationLogTest.java @@ -0,0 +1,54 @@ +/* + * 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.consumer.html; + +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.vertx.core.json.JsonObject; +import org.junit.jupiter.api.Test; + +class GraphNodeOperationLogTest { + + @Test + void validateEmpty() { + // given + GraphNodeOperationLog log = GraphNodeOperationLog.empty(); + + // when + GraphNodeOperationLog result = new GraphNodeOperationLog(log.toJson()); + + // then + assertTrue(result.getFactory().isEmpty()); + assertTrue(result.getData().isEmpty()); + } + + @Test + void validateSerialization() { + // given + String factory = "factory"; + JsonObject data = new JsonObject().put("key", "value"); + + GraphNodeOperationLog log = GraphNodeOperationLog.newInstance(factory, data); + + // when + GraphNodeOperationLog result = new GraphNodeOperationLog(log.toJson()); + + // then + assertEquals(factory, result.getFactory()); + assertEquals(data, result.getData()); + } +} \ No newline at end of file diff --git a/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/GraphNodeResponseLogTest.java b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/GraphNodeResponseLogTest.java new file mode 100644 index 00000000..8568b764 --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/html/GraphNodeResponseLogTest.java @@ -0,0 +1,43 @@ +/* + * 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.consumer.html; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.knotx.fragments.engine.api.node.single.FragmentResult; +import io.vertx.core.json.JsonArray; +import org.junit.jupiter.api.Test; + +class GraphNodeResponseLogTest { + + @Test + void validateSerialization() { + // given + String transition = FragmentResult.SUCCESS_TRANSITION; + + JsonArray invocations = new JsonArray().add("invocation"); + + GraphNodeResponseLog log = GraphNodeResponseLog + .newInstance(transition, invocations); + + // when + GraphNodeResponseLog result = new GraphNodeResponseLog(log.toJson()); + + // then + assertEquals(transition, result.getTransition()); + assertEquals(invocations, result.getInvocations()); + } +} \ No newline at end of file diff --git a/handler/core/src/test/java/io/knotx/fragments/handler/consumer/metadata/EventLogConverterTest.java b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/metadata/EventLogConverterTest.java new file mode 100644 index 00000000..f0f5b5c9 --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/metadata/EventLogConverterTest.java @@ -0,0 +1,184 @@ +/* + * 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.consumer.metadata; + +import static io.knotx.fragments.engine.api.node.single.FragmentResult.ERROR_TRANSITION; +import static io.knotx.fragments.engine.api.node.single.FragmentResult.SUCCESS_TRANSITION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.knotx.fragments.api.Fragment; +import io.knotx.fragments.engine.EventLogEntry; +import io.knotx.fragments.engine.api.node.single.FragmentResult; +import io.knotx.fragments.handler.LoggedNodeStatus; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import java.util.Arrays; +import java.util.Collections; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class EventLogConverterTest { + + private static final String TASK_NAME = "test-event-log"; + private static final String NODE_ID = "1234-4321-1234"; + + @Test + @DisplayName("Expect status=UNPROCESSED when log does not contain any entries") + void fillWithEmptyLog() { + EventLogConverter tested = givenEmptyLogConverter(); + + NodeExecutionData result = tested.getExecutionData(NODE_ID); + + assertEquals(LoggedNodeStatus.UNPROCESSED, result.getStatus()); + } + + @Test + @DisplayName("Expect status=UNPROCESSED when log does not contain entries for the given node") + void fillWithMissingLogEntries() { + EventLogConverter tested = givenLogConverter( + EventLogEntry.success(TASK_NAME, "non-existent", successFragmentResult()) + ); + + NodeExecutionData result = tested.getExecutionData(NODE_ID); + + assertEquals(LoggedNodeStatus.UNPROCESSED, result.getStatus()); + } + + @Test + @DisplayName("Expect status=SUCCESS when single success log entry for node") + void fillWithSingleSuccessLogEntry() { + EventLogEntry[] logs = new EventLogEntry[]{ + EventLogEntry.success(TASK_NAME, NODE_ID, successFragmentResult(nodeLog())), + EventLogEntry.success(TASK_NAME, "non-existent", successFragmentResult()), + EventLogEntry.error(TASK_NAME, "non-existent", "timeout"), + }; + EventLogConverter tested = givenLogConverter(logs); + + NodeExecutionData result = tested.getExecutionData(NODE_ID); + + assertEquals(LoggedNodeStatus.SUCCESS, result.getStatus()); + assertNotNull(result.getResponse()); + assertEquals(SUCCESS_TRANSITION, result.getResponse().getTransition()); + assertEquals(new JsonArray().add(nodeLog()), result.getResponse().getInvocations()); + } + + @Test + @DisplayName("Expect status=ERROR when single error log entry for node") + void fillWithSingleErrorLogEntry() { + EventLogEntry[] logs = new EventLogEntry[]{ + EventLogEntry.error(TASK_NAME, NODE_ID, ERROR_TRANSITION), + EventLogEntry.success(TASK_NAME, "non-existent", successFragmentResult()), + EventLogEntry.error(TASK_NAME, "non-existent", "timeout"), + }; + EventLogConverter tested = givenLogConverter(logs); + + NodeExecutionData result = tested.getExecutionData(NODE_ID); + + assertEquals(LoggedNodeStatus.ERROR, result.getStatus()); + assertNotNull(result.getResponse()); + assertEquals(ERROR_TRANSITION, result.getResponse().getTransition()); + assertEquals(new JsonArray(), result.getResponse().getInvocations()); + } + + @Test + @DisplayName("Expect status=OTHER when there's a single success log entry with custom transition") + void fillWithSingleSuccessCustomLogEntry() { + EventLogEntry[] logs = new EventLogEntry[]{ + EventLogEntry.success(TASK_NAME, NODE_ID, successFragmentResult(nodeLog(), true)), + EventLogEntry.success(TASK_NAME, "non-existent", successFragmentResult()), + EventLogEntry.error(TASK_NAME, "non-existent", "timeout"), + }; + EventLogConverter tested = givenLogConverter(logs); + + NodeExecutionData result = tested.getExecutionData(NODE_ID); + + assertEquals(LoggedNodeStatus.OTHER, result.getStatus()); + assertNotNull(result.getResponse()); + assertEquals("custom", result.getResponse().getTransition()); + assertEquals(new JsonArray().add(nodeLog()), result.getResponse().getInvocations()); + } + + @Test + @DisplayName("Expect status=SUCCESS when there's an unsupported success transition") + void fillWithDoubleLogSuccessEntry() { + EventLogEntry[] logs = new EventLogEntry[]{ + EventLogEntry.success(TASK_NAME, NODE_ID, successFragmentResult(nodeLog())), + EventLogEntry.unsupported(TASK_NAME, NODE_ID, SUCCESS_TRANSITION), + EventLogEntry.success(TASK_NAME, "non-existent", successFragmentResult()), + EventLogEntry.error(TASK_NAME, "non-existent", "timeout"), + }; + EventLogConverter tested = givenLogConverter(logs); + + NodeExecutionData result = tested.getExecutionData(NODE_ID); + + assertEquals(LoggedNodeStatus.SUCCESS, result.getStatus()); + assertNotNull(result.getResponse()); + assertEquals(SUCCESS_TRANSITION, result.getResponse().getTransition()); + assertEquals(new JsonArray().add(nodeLog()), result.getResponse().getInvocations()); + } + + @Test + @DisplayName("Expect unsupported non-success transition to have no effect") + void fillWithDoubleLogNonSuccessEntry() { + EventLogEntry[] logs = new EventLogEntry[]{ + EventLogEntry.error(TASK_NAME, NODE_ID, ERROR_TRANSITION), + EventLogEntry.unsupported(TASK_NAME, NODE_ID, ERROR_TRANSITION), + EventLogEntry.success(TASK_NAME, "non-existent", successFragmentResult()), + EventLogEntry.error(TASK_NAME, "non-existent", "timeout"), + }; + EventLogConverter tested = givenLogConverter(logs); + + NodeExecutionData result = tested.getExecutionData(NODE_ID); + + assertEquals(LoggedNodeStatus.ERROR, result.getStatus()); + assertNotNull(result.getResponse()); + assertEquals(ERROR_TRANSITION, result.getResponse().getTransition()); + assertEquals(new JsonArray(), result.getResponse().getInvocations()); + } + + EventLogConverter givenEmptyLogConverter() { + return new EventLogConverter(Collections.emptyList()); + } + + EventLogConverter givenLogConverter(EventLogEntry... entries) { + return new EventLogConverter(Arrays.asList(entries)); + } + + private FragmentResult successFragmentResult() { + return successFragmentResult(null); + } + + private FragmentResult successFragmentResult(JsonObject nodeLog) { + return successFragmentResult(nodeLog, false); + } + + private FragmentResult successFragmentResult(JsonObject nodeLog, boolean customTransition) { + return new FragmentResult( + new Fragment("dummy", new JsonObject(), ""), + customTransition ? "custom" : SUCCESS_TRANSITION, + nodeLog + ); + } + + private JsonObject nodeLog() { + return new JsonObject() + .put("alias", "alias") + .put("logs", new JsonObject()) + .put("doActionLogs", new JsonArray()); + } + +} diff --git a/handler/core/src/test/java/io/knotx/fragments/handler/consumer/metadata/MetadataConverterTest.java b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/metadata/MetadataConverterTest.java new file mode 100644 index 00000000..2b1d247d --- /dev/null +++ b/handler/core/src/test/java/io/knotx/fragments/handler/consumer/metadata/MetadataConverterTest.java @@ -0,0 +1,524 @@ +/* + * 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.consumer.metadata; + +import static io.knotx.fragments.engine.api.node.single.FragmentResult.ERROR_TRANSITION; +import static io.knotx.fragments.engine.api.node.single.FragmentResult.SUCCESS_TRANSITION; +import static io.knotx.junit5.assertions.KnotxAssertions.assertJsonEquals; + +import com.google.common.collect.ImmutableMap; +import io.knotx.fragments.api.Fragment; +import io.knotx.fragments.engine.EventLog; +import io.knotx.fragments.engine.EventLogEntry; +import io.knotx.fragments.engine.FragmentEvent; +import io.knotx.fragments.engine.NodeMetadata; +import io.knotx.fragments.engine.OperationMetadata; +import io.knotx.fragments.engine.TaskMetadata; +import io.knotx.fragments.engine.api.node.NodeType; +import io.knotx.fragments.engine.api.node.single.FragmentResult; +import io.knotx.fragments.handler.LoggedNodeStatus; +import io.knotx.fragments.handler.consumer.html.GraphNodeOperationLog; +import io.knotx.fragments.task.factory.node.action.ActionNodeFactory; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; +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; + +@ExtendWith(MockitoExtension.class) +class MetadataConverterTest { + + private static final String TASK_NAME = "some-task"; + private static final String ROOT_NODE = "1234-4321-1234"; + + private MetadataConverter tested; + + @Test + @DisplayName("Expect empty json when not defined TaskMetadata provided") + void shouldProduceEmptyJsonWhenNoMetadataProvided() { + givenNotDefinedTaskMetadata(); + + JsonObject output = tested.getExecutionLog().toJson(); + + assertJsonEquals(new JsonObject(), output); + } + + @Test + @DisplayName("Expect only processing info and node id when TaskMetadata with no metadata provided") + void shouldProduceOnlyProcessingInfoWhenNoMetadataProvided() { + givenNoMetadata(ROOT_NODE); + + JsonObject output = tested.getExecutionLog().toJson(); + + JsonObject expected = jsonForNotDescribedNode(ROOT_NODE); + + assertJsonEquals(expected, output); + } + + @Test + @DisplayName("Expect correct JSON when metadata with one node provided") + void shouldProduceCorrectJsonForOneNodeMetadata() { + givenNodesMetadata(ROOT_NODE, simpleNode(ROOT_NODE, "custom")); + + JsonObject output = tested.getExecutionLog().toJson(); + + JsonObject expected = jsonForNode(ROOT_NODE, "custom"); + + assertJsonEquals(expected, output); + } + + @Test + @DisplayName("Expect correct JSON when metadata with one action node provided") + void shouldProduceCorrectJsonForOneActionNodeMetadata() { + givenNodesMetadata(ROOT_NODE, simpleNode(ROOT_NODE, "action")); + + JsonObject output = tested.getExecutionLog().toJson(); + + JsonObject expected = jsonForActionNode(ROOT_NODE); + + assertJsonEquals(expected, output); + } + + @Test + @DisplayName("Expect correct JSON when metadata with two nodes with transition provided") + void shouldProduceCorrectJsonForTwoNodesWithTransition() { + givenNodesMetadata(ROOT_NODE, + simpleNode(ROOT_NODE, "custom", ImmutableMap.of(SUCCESS_TRANSITION, "node-A")), + simpleNode("node-A", "factory-A") + ); + + JsonObject output = tested.getExecutionLog().toJson(); + + JsonObject expected = jsonForNode(ROOT_NODE, "custom") + .put("on", new JsonObject() + .put(SUCCESS_TRANSITION, jsonForNode("node-A", "factory-A"))); + + assertJsonEquals(expected, output); + } + + @Test + @DisplayName("Expect missing node metadata when transition returned in log that was not described") + void shouldProduceCorrectJsonForMissingNodeCase() { + EventLogEntry[] logs = new EventLogEntry[]{ + EventLogEntry.error(TASK_NAME, ROOT_NODE, ERROR_TRANSITION), + EventLogEntry.unsupported(TASK_NAME, ROOT_NODE, ERROR_TRANSITION) + }; + + givenEventLogAndNodesMetadata(createEventLog(logs), ROOT_NODE, + simpleNode(ROOT_NODE, "custom", ImmutableMap.of(SUCCESS_TRANSITION, "node-A")), + simpleNode("node-A", "factory-A") + ); + + JsonObject output = tested.getExecutionLog().toJson(); + + String missingNodeId = output.getJsonObject("on").getJsonObject(ERROR_TRANSITION) + .getString("id"); + + JsonObject expected = jsonForNode(ROOT_NODE, "custom") + .put("status", LoggedNodeStatus.ERROR) + .put("on", new JsonObject() + .put(SUCCESS_TRANSITION, jsonForNode("node-A", "factory-A")) + .put(ERROR_TRANSITION, jsonForMissingNode(missingNodeId))) + .put("response", new JsonObject() + .put("transition", ERROR_TRANSITION) + .put("invocations", new JsonArray())); + + assertJsonEquals(expected, output); + } + + @Test + @DisplayName("Expect no missing node metadata when _success transition and no next node defined") + void shouldProduceCorrectJsonForSuccessTransitionWithoutNextNode() { + EventLogEntry[] logs = new EventLogEntry[]{ + EventLogEntry.success(TASK_NAME, ROOT_NODE, + createFragmentResult(SUCCESS_TRANSITION, simpleNodeLog())), + }; + + givenEventLogAndNodesMetadata(createEventLog(logs), ROOT_NODE, + simpleNode(ROOT_NODE, "custom") + ); + + JsonObject output = tested.getExecutionLog().toJson(); + + JsonObject expected = jsonForNode(ROOT_NODE, "custom") + .put("status", LoggedNodeStatus.SUCCESS) + .put("response", new JsonObject() + .put("transition", SUCCESS_TRANSITION) + .put("invocations", wrap(simpleNodeLog()))); + + assertJsonEquals(expected, output); + } + + @Test + @DisplayName("Expect correct JSON when metadata with nested nodes provided") + void shouldProduceCorrectJsonForCompositeNodeWithSimpleNodes() { + givenNodesMetadata(ROOT_NODE, + compositeNode(ROOT_NODE, "custom", "node-A", "node-B", "node-C"), + simpleNode("node-A", "factory-A"), + simpleNode("node-B", "factory-B"), + simpleNode("node-C", "factory-C") + ); + + JsonObject output = tested.getExecutionLog().toJson(); + + JsonObject expected = jsonForNode(ROOT_NODE, "custom") + .put("type", NodeType.COMPOSITE) + .put("label", "composite") + .put("subtasks", new JsonArray(Arrays.asList( + jsonForNode("node-A", "factory-A"), + jsonForNode("node-B", "factory-B"), + jsonForNode("node-C", "factory-C") + ))); + + assertJsonEquals(expected, output); + } + + @Test + @DisplayName("Expect correct JSON when metadata with nested nodes provided and some are not described") + void shouldProduceCorrectJsonForCompositeNodeWithSimpleNodesNotAllDescribed() { + givenNodesMetadata(ROOT_NODE, + compositeNode(ROOT_NODE, "custom", "node-A", "node-B", "node-C"), + simpleNode("node-A", "factory-A"), + simpleNode("node-B", "factory-B"), + simpleNode("node-C", "factory-C") + ); + + JsonObject output = tested.getExecutionLog().toJson(); + + JsonObject expected = jsonForNode(ROOT_NODE, "custom") + .put("type", NodeType.COMPOSITE) + .put("label", "composite") + .put("subtasks", new JsonArray(Arrays.asList( + jsonForNode("node-A", "factory-A"), + jsonForNotDescribedNode("node-B"), + jsonForNotDescribedNode("node-C") + ))); + + assertJsonEquals(expected, output); + } + + @Test + @DisplayName("Expect correct JSON when full metadata for complex graph provided") + void shouldProduceCorrectJsonForComplexGraphWithFullMetadata() { + givenNodesMetadata(ROOT_NODE, + simpleNode(ROOT_NODE, "custom", + ImmutableMap.of(SUCCESS_TRANSITION, "node-A", "_failure", "node-B")), + simpleNode("node-A", "factory-A"), + compositeSubtasksNode("node-B", + ImmutableMap + .of(SUCCESS_TRANSITION, "node-C", ERROR_TRANSITION, "node-D", "_timeout", "node-E"), + "node-B1", "node-B2", "node-B3" + ), + simpleNode("node-B1", "action", ImmutableMap.of(SUCCESS_TRANSITION, "node-B1-special")), + simpleNode("node-B1-special", "factory-B1-special"), + simpleNode("node-B2", "factory-B2"), + simpleNode("node-B3", "factory-B3"), + simpleNode("node-C", "factory-C"), + simpleNode("node-D", "factory-D"), + simpleNode("node-E", "factory-E") + ); + + JsonObject output = tested.getExecutionLog().toJson(); + + JsonObject expected = jsonForNode(ROOT_NODE, "custom") + .put("on", new JsonObject() + .put(SUCCESS_TRANSITION, jsonForNode("node-A", "factory-A")) + .put("_failure", jsonForNode("node-B", "subtasks") + .put("type", NodeType.COMPOSITE) + .put("label", "composite") + .put("on", new JsonObject() + .put(SUCCESS_TRANSITION, jsonForNode("node-C", "factory-C")) + .put(ERROR_TRANSITION, jsonForNode("node-D", "factory-D")) + .put("_timeout", jsonForNode("node-E", "factory-E")) + ) + .put("subtasks", new JsonArray(Arrays.asList( + jsonForActionNode("node-B1") + .put("on", new JsonObject() + .put(SUCCESS_TRANSITION, + jsonForNode("node-B1-special", "factory-B1-special"))), + jsonForNode("node-B2", "factory-B2"), + jsonForNode("node-B3", "factory-B3") + ))) + )); + + assertJsonEquals(expected, output); + } + + @Test + @DisplayName("Expect correct JSON when full metadata for complex graph provided with event log") + void shouldProduceCorrectJsonForComplexGraphWithFullMetadataWithEventLog() { + EventLog eventLog = createEventLog( + EventLogEntry.success(TASK_NAME, "a-node", + createFragmentResult(SUCCESS_TRANSITION, simpleNodeLog())), + EventLogEntry.success(TASK_NAME, "b1-subgraph", + createFragmentResult(SUCCESS_TRANSITION, simpleNodeLog())), + EventLogEntry + .success(TASK_NAME, "b2-subgraph", createFragmentResult("_fallback", complexNodeLog())), + EventLogEntry.unsupported(TASK_NAME, "b2-subgraph", "_fallback"), + EventLogEntry.error(TASK_NAME, "b-composite", ERROR_TRANSITION), + EventLogEntry + .success(TASK_NAME, "f-node", createFragmentResult(SUCCESS_TRANSITION, simpleNodeLog())) + ); + + givenEventLogAndNodesMetadata( + eventLog, + "a-node", + simpleNode("a-node", "action", + ImmutableMap.of(SUCCESS_TRANSITION, "b-composite", ERROR_TRANSITION, "c-node")), + compositeSubtasksNode("b-composite", + ImmutableMap.of(SUCCESS_TRANSITION, "e-node", ERROR_TRANSITION, "f-node"), + "b1-subgraph", "b2-subgraph" + ), + simpleNode("b1-subgraph", "action"), + simpleNode("b2-subgraph", "action", ImmutableMap.of(SUCCESS_TRANSITION, "d-node")), + simpleNode("c-node", "action"), + simpleNode("d-node", "action"), + simpleNode("e-node", "action"), + simpleNode("f-node", "action") + ); + + JsonObject output = tested.getExecutionLog().toJson(); + + String missingNodeId = output.getJsonObject("on").getJsonObject(SUCCESS_TRANSITION) + .getJsonArray("subtasks").getJsonObject(1).getJsonObject("on").getJsonObject("_fallback") + .getString("id"); + + JsonObject expected = jsonForActionNode("a-node") + .put("response", new JsonObject() + .put("transition", SUCCESS_TRANSITION) + .put("invocations", wrap(simpleNodeLog()))) + .put("status", LoggedNodeStatus.SUCCESS) + .put("on", new JsonObject() + .put(ERROR_TRANSITION, jsonForActionNode("c-node")) + .put(SUCCESS_TRANSITION, jsonForNode("b-composite", "subtasks") + .put("response", new JsonObject() + .put("transition", ERROR_TRANSITION) + .put("invocations", new JsonArray())) + .put("status", LoggedNodeStatus.ERROR) + .put("type", NodeType.COMPOSITE) + .put("label", "composite") + .put("on", new JsonObject() + .put(SUCCESS_TRANSITION, jsonForActionNode("e-node")) + .put(ERROR_TRANSITION, jsonForActionNode("f-node") + .put("response", new JsonObject() + .put("transition", SUCCESS_TRANSITION) + .put("invocations", wrap(simpleNodeLog()))) + .put("status", LoggedNodeStatus.SUCCESS) + ) + ) + .put("subtasks", new JsonArray(Arrays.asList( + jsonForActionNode("b1-subgraph") + .put("response", new JsonObject() + .put("transition", SUCCESS_TRANSITION) + .put("invocations", wrap(simpleNodeLog()))) + .put("status", LoggedNodeStatus.SUCCESS), + jsonForActionNode("b2-subgraph") + .put("on", new JsonObject() + .put(SUCCESS_TRANSITION, jsonForActionNode("d-node")) + .put("_fallback", jsonForMissingNode(missingNodeId))) + .put("response", new JsonObject() + .put("transition", "_fallback") + .put("invocations", wrap(complexNodeLog()))) + .put("status", LoggedNodeStatus.OTHER) + ))) + )); + + assertJsonEquals(expected, output); + } + + private EventLog createEventLog(EventLogEntry... entries) { + return new EventLog(new JsonObject() + .put("operations", new JsonArray( + Arrays.stream(entries).map(EventLogEntry::toJson).collect(Collectors.toList())))); + } + + private FragmentResult createFragmentResult(String transition, JsonObject nodeLog) { + return new FragmentResult(new Fragment("_empty", new JsonObject(), ""), transition, nodeLog); + } + + private void givenNotDefinedTaskMetadata() { + tested = new MetadataConverter(emptyFragmentEvent(), TaskMetadata.notDefined()); + } + + private void givenNoMetadata(String rootNodeId) { + tested = new MetadataConverter(emptyFragmentEvent(), + TaskMetadata.noMetadata(TASK_NAME, rootNodeId)); + } + + private void givenNodesMetadata(String rootNodeId, NodeMetadata... nodes) { + givenEventLogAndNodesMetadata(new EventLog(), rootNodeId, nodes); + } + + private void givenEventLogAndNodesMetadata(EventLog eventLog, String rootNodeId, + NodeMetadata... nodes) { + Map metadata = new HashMap<>(); + for (NodeMetadata node : nodes) { + metadata.put(node.getNodeId(), node); + } + + tested = new MetadataConverter(emptyFragmentEvent(eventLog), + TaskMetadata.create(TASK_NAME, rootNodeId, metadata)); + } + + private FragmentEvent emptyFragmentEvent() { + return emptyFragmentEvent(new EventLog()); + } + + private FragmentEvent emptyFragmentEvent(EventLog eventLog) { + FragmentEvent output = new FragmentEvent(new Fragment("dummy", new JsonObject(), "")); + output.appendLog(eventLog); + return output; + } + + private NodeMetadata simpleNode(String id, String factory) { + return simpleNode(id, factory, ImmutableMap.of()); + } + + private NodeMetadata simpleNode(String id, String factory, Map transitions) { + return NodeMetadata.single( + id, + "simple", + transitions, + getSampleConfigFor(factory) + ); + } + + private OperationMetadata getSampleConfigFor(String factory) { + final OperationMetadata result; + if (ActionNodeFactory.NAME.equals(factory)) { + result = new OperationMetadata(factory, new JsonObject().put("actionFactory", "http")); + } else { + result = new OperationMetadata(factory); + } + return result; + } + + private NodeMetadata compositeSubtasksNode(String id, Map transitions, + String... nested) { + return compositeNode(id, "subtasks", transitions, nested); + } + + private NodeMetadata compositeNode(String id, String factory, String... nested) { + return compositeNode(id, factory, ImmutableMap.of(), nested); + } + + private NodeMetadata compositeNode(String id, String factory, Map transitions, + String... nested) { + return NodeMetadata.composite( + id, + "composite", + transitions, + Arrays.asList(nested), + getSampleConfigFor(factory) + ); + } + + private JsonObject jsonForOtherNode(String id) { + return new JsonObject() + .put("id", id) + .put("label", "!") + .put("type", NodeType.SINGLE) + .put("status", LoggedNodeStatus.OTHER); + } + + private JsonObject jsonForMissingNode(String id) { + return new JsonObject() + .put("id", id) + .put("label", "!") + .put("type", NodeType.SINGLE) + .put("status", LoggedNodeStatus.MISSING); + } + + private JsonObject jsonForNode(String id, String factory) { + return new JsonObject() + .put("id", id) + .put("label", "simple") + .put("type", NodeType.SINGLE) + .put("on", new JsonObject()) + .put("subtasks", new JsonArray()) + .put("operation", GraphNodeOperationLog.newInstance(factory, new JsonObject()).toJson()) + .put("status", LoggedNodeStatus.UNPROCESSED); + } + + private JsonObject jsonForActionNode(String id) { + return jsonForNode(id, "action") + .put("operation", + GraphNodeOperationLog + .newInstance("action", new JsonObject().put("actionFactory", "http")) + .toJson()); + } + + private JsonObject jsonForNotDescribedNode(String id) { + return new JsonObject() + .put("id", id) + .put("status", LoggedNodeStatus.UNPROCESSED); + } + + private JsonArray wrap(JsonObject instance) { + return new JsonArray(Collections.singletonList(instance)); + } + + private JsonObject simpleNodeLog() { + return new JsonObject() + .put("alias", "alias") + .put("started", 123123213) + .put("finished", 21342342) + .put("operation", "http") + .put("logs", new JsonObject()); + } + + private JsonObject complexNodeLog() { + return new JsonObject() + .put("alias", "cb-my-payments") + .put("started", 123123213) + .put("finished", 21342342) + .put("operation", new JsonObject() + .put("type", "action") + .put("factory", "cb")) + .put("logs", new JsonObject()) + .put("invocations", new JsonArray(Arrays.asList( + new JsonObject() + .put("alias", "my-payments") + .put("started", 123123213) + .put("finished", 21342342) + .put("operation", new JsonObject() + .put("type", "action") + .put("factory", "http")) + .put("logs", new JsonObject() + .put("request", new JsonObject()) + .put("response", new JsonObject() + .put("statusCode", 500))), + new JsonObject() + .put("alias", "my-payments") + .put("started", 123123213) + .put("finished", 21342342) + .put("operation", new JsonObject() + .put("type", "action") + .put("factory", "http")) + .put("logs", new JsonObject() + .put("request", new JsonObject()) + .put("response", new JsonObject() + .put("statusCode", 500))) + ) + )); + } +} 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 index 67860690..b6b75c29 100644 --- 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 @@ -16,18 +16,28 @@ package io.knotx.fragments.task.factory; import static io.knotx.fragments.engine.api.node.single.FragmentResult.SUCCESS_TRANSITION; +import static io.knotx.fragments.task.factory.DefaultTaskFactoryConfig.DEFAULT_TASK_NAME_KEY; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonMap; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import io.knotx.fragments.handler.api.exception.ConfigurationException; import io.knotx.fragments.api.Fragment; import io.knotx.fragments.engine.FragmentEvent; import io.knotx.fragments.engine.FragmentEventContext; -import io.knotx.fragments.engine.api.Task; -import io.knotx.fragments.engine.api.node.composite.CompositeNode; +import io.knotx.fragments.engine.NodeMetadata; +import io.knotx.fragments.engine.OperationMetadata; +import io.knotx.fragments.engine.TaskMetadata; +import io.knotx.fragments.engine.TaskWithMetadata; import io.knotx.fragments.engine.api.node.Node; +import io.knotx.fragments.engine.api.node.NodeType; +import io.knotx.fragments.engine.api.node.composite.CompositeNode; import io.knotx.fragments.engine.api.node.single.SingleNode; +import io.knotx.fragments.handler.api.actionlog.ActionLogLevel; +import io.knotx.fragments.handler.api.exception.ConfigurationException; 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; @@ -36,12 +46,12 @@ 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 java.util.Optional; -import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -54,89 +64,94 @@ @MockitoSettings(strictness = Strictness.LENIENT) class DefaultTaskFactoryTest { + private static final String MY_TASK_KEY = "myTaskKey"; private static final String TASK_NAME = "task"; - private static final String COMPOSITE_NODE_ID = "composite"; + private static final String TEST_ACTION_FACTORY = "test-action"; 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()); + private FragmentEventContext fragmentEvent; + private FragmentEventContext fragmentEventWithCustomTaskKey; + + @BeforeEach + void configure() { + fragmentEvent = + new FragmentEventContext(new FragmentEvent(new Fragment("type", + new JsonObject().put(DEFAULT_TASK_NAME_KEY, TASK_NAME), "body")), + new ClientRequest()); + fragmentEventWithCustomTaskKey = + 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.") + @DisplayName("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 FragmentEvent(new Fragment("type", new JsonObject(), "body")), new ClientRequest() ); DefaultTaskFactory tested = new DefaultTaskFactory() - .configure(emptyFactoryConfig().toJson(), vertx); + .configure(taskFactoryConfig().toJson(), vertx); // when boolean accepted = tested.accept(fragmentWithNoTask); // then - Assertions.assertFalse(accepted); + assertFalse(accepted); } @Test - @DisplayName("Expect fragment is not accepted when it specifies a task but it is not configured") - void noAcceptFragmentWhenTaskNotConfigured(Vertx vertx) { + @DisplayName("Fragment is not accepted when it specifies a task but it is not configured.") + void notAcceptFragmentWhenTaskNotConfigured(Vertx vertx) { // given DefaultTaskFactory tested = new DefaultTaskFactory() - .configure(emptyFactoryConfig().toJson(), vertx); + .configure(taskFactoryConfig().toJson(), vertx); // when - boolean accepted = tested.accept(SAMPLE_FRAGMENT_EVENT); + boolean accepted = tested.accept(fragmentEvent); // then - Assertions.assertFalse(accepted); + assertFalse(accepted); } @Test - @DisplayName("Expect fragment is accepted when it specifies a task and it is configured") + @DisplayName("Fragment is accepted when it specifies a task and it is configured.") void acceptFragment(Vertx vertx) { - GraphNodeOptions graph = new GraphNodeOptions("A", NO_TRANSITIONS); + // given + GraphNodeOptions rootNodeOptions = new GraphNodeOptions("A", NO_TRANSITIONS); DefaultTaskFactory tested = new DefaultTaskFactory().configure( - createTaskFactoryConfig(graph, new JsonObject()).toJson(), vertx); + taskFactoryConfig(rootNodeOptions).toJson(), vertx); // when - boolean accepted = tested.accept(SAMPLE_FRAGMENT_EVENT); + boolean accepted = tested.accept(fragmentEvent); // then - Assertions.assertTrue(accepted); + assertTrue(accepted); } @Test - @DisplayName("Expect fragment is accepted when it specifies a custom task name and it is configured") + @DisplayName("Fragment is accepted when it specifies a custom task name.") void acceptFragmentWhenCustomTaskName(Vertx vertx) { - GraphNodeOptions graph = new GraphNodeOptions("A", NO_TRANSITIONS); + // given + GraphNodeOptions rootNodeOptions = new GraphNodeOptions("A", NO_TRANSITIONS); DefaultTaskFactory tested = new DefaultTaskFactory().configure( - createTaskFactoryConfig(graph, new JsonObject()).setTaskNameKey(MY_TASK_KEY).toJson(), + taskFactoryConfig(rootNodeOptions).setTaskNameKey(MY_TASK_KEY).toJson(), vertx); // when - boolean accepted = tested.accept(SAMPLE_FRAGMENT_EVENT_WITH_CUSTOM_TASK_KEY); + boolean accepted = tested.accept(fragmentEventWithCustomTaskKey); // then - Assertions.assertTrue(accepted); + assertTrue(accepted); } @Test - @DisplayName("Expect task not found exception when a fragment does not define a task.") + @DisplayName("Configuration exception is thrown when a fragment does not define a task.") void newInstanceFailedWhenNoTask(Vertx vertx) { // given FragmentEventContext fragmentWithNoTask = @@ -145,48 +160,47 @@ void newInstanceFailedWhenNoTask(Vertx vertx) { new Fragment("type", new JsonObject(), "body")), new ClientRequest() ); - JsonObject actionNodeConfig = createActionNodeConfig("A", SUCCESS_TRANSITION); - GraphNodeOptions graph = new GraphNodeOptions("A", NO_TRANSITIONS); + GraphNodeOptions rootNodeOptions = new GraphNodeOptions("A", NO_TRANSITIONS); + DefaultTaskFactory tested = new DefaultTaskFactory() + .configure(taskFactoryConfig(rootNodeOptions, actionNodeFactoryConfig("A")).toJson(), + vertx); - // when - Assertions.assertThrows( + // when, then + assertThrows( ConfigurationException.class, - () -> new DefaultTaskFactory() - .configure(createTaskFactoryConfig(graph, actionNodeConfig).toJson(), vertx) - .newInstance(fragmentWithNoTask)); + () -> tested.newInstanceWithMetadata(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); + GraphNodeOptions rootNodeOptions = new GraphNodeOptions("A", NO_TRANSITIONS); // when - Task task = new DefaultTaskFactory() - .configure(createTaskFactoryConfig(graph, actionNodeConfig).toJson(), vertx) - .newInstance(SAMPLE_FRAGMENT_EVENT); + TaskWithMetadata task = new DefaultTaskFactory() + .configure(taskFactoryConfig(rootNodeOptions, actionNodeFactoryConfig("A")).toJson(), vertx) + .newInstanceWithMetadata(fragmentEvent); // then - assertEquals(TASK_NAME, task.getName()); + assertEquals(TASK_NAME, task.getTask().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); + GraphNodeOptions rootNodeOptions = new GraphNodeOptions("A", NO_TRANSITIONS); // when DefaultTaskFactory taskFactory = new DefaultTaskFactory() - .configure(createTaskFactoryConfig(graph, actionNodeConfig).toJson(), vertx); + .configure(taskFactoryConfig(rootNodeOptions, actionNodeFactoryConfig("A")).toJson(), + vertx); // then assertNotSame( - taskFactory.newInstance(SAMPLE_FRAGMENT_EVENT), - taskFactory.newInstance(SAMPLE_FRAGMENT_EVENT) + taskFactory.newInstanceWithMetadata(fragmentEvent), + taskFactory.newInstanceWithMetadata(fragmentEvent) ); } @@ -194,55 +208,105 @@ void newInstanceAlways(Vertx vertx) { @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); + GraphNodeOptions rootNodeOptions = new GraphNodeOptions("A", NO_TRANSITIONS); // when - Task task = new DefaultTaskFactory() + TaskWithMetadata task = new DefaultTaskFactory() .configure( - createTaskFactoryConfig(graph, actionNodeConfig).setTaskNameKey(MY_TASK_KEY).toJson(), + taskFactoryConfig(rootNodeOptions, actionNodeFactoryConfig("A")) + .setTaskNameKey(MY_TASK_KEY) + .toJson(), vertx) - .newInstance(SAMPLE_FRAGMENT_EVENT_WITH_CUSTOM_TASK_KEY); + .newInstanceWithMetadata(fragmentEventWithCustomTaskKey); // then - assertEquals(TASK_NAME, task.getName()); + assertEquals(TASK_NAME, task.getTask().getName()); + } + + @Test + @DisplayName("Expect task metadata when INFO is configured.") + void expectMetadata(Vertx vertx) { + // given + JsonObject actionNodeConfig = infoLevel(actionNodeFactoryConfig("A")); + GraphNodeOptions rootNodeOptions = new GraphNodeOptions("A", NO_TRANSITIONS); + + // when + TaskWithMetadata taskWithMetadata = new DefaultTaskFactory() + .configure( + taskFactoryConfig(rootNodeOptions, actionNodeConfig) + .toJson(), + vertx) + .newInstanceWithMetadata(fragmentEvent); + + ActionFactoryOptions actionOptions = new ActionNodeFactoryConfig(actionNodeConfig).getActions() + .get("A"); + + // then + TaskMetadata metadata = taskWithMetadata.getMetadata(); + assertEquals(TASK_NAME, metadata.getTaskName()); + + NodeMetadata rootMetadata = metadata.getNodesMetadata().get(metadata.getRootNodeId()); + assertTrue(rootMetadata.getTransitions().isEmpty()); + assertTrue(rootMetadata.getNestedNodes().isEmpty()); + assertEquals(NodeType.SINGLE, rootMetadata.getType()); + + OperationMetadata operation = rootMetadata.getOperation(); + assertActionNodeMetadata(operation, "A", actionOptions.getConfig()); } @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); + JsonObject options = actionNodeFactoryConfig("A", "B"); - GraphNodeOptions graph = new GraphNodeOptions("A", Collections - .singletonMap("customTransition", + GraphNodeOptions rootNodeOptions = new GraphNodeOptions("A", + singletonMap("customTransition", new GraphNodeOptions("B", NO_TRANSITIONS))); // when - Task task = getTask(graph, options, vertx); + TaskWithMetadata taskWithMetadata = getTaskWithMetadata(rootNodeOptions, options, vertx); // then - assertEquals(TASK_NAME, task.getName()); + assertEquals(TASK_NAME, taskWithMetadata.getTask().getName()); - assertTrue(task.getRootNode().isPresent()); - Node rootNode = task.getRootNode().get(); - assertTrue(rootNode instanceof SingleNode); - assertEquals("A", rootNode.getId()); + assertTrue(taskWithMetadata.getTask().getRootNode().isPresent()); + Node rootNode = taskWithMetadata.getTask().getRootNode().get(); 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 metadata of two action nodes with transition between.") + void expectMetadataForNodesWithTransitionBetween(Vertx vertx) { + // given + JsonObject options = infoLevel(actionNodeFactoryConfig("A", "B")); + + GraphNodeOptions rootNodeOptions = new GraphNodeOptions("A", singletonMap("customTransition", + new GraphNodeOptions("B", NO_TRANSITIONS))); + + // when + TaskMetadata metadata = getTaskWithMetadata(rootNodeOptions, options, vertx).getMetadata(); + + // then + assertEquals(TASK_NAME, metadata.getTaskName()); + + NodeMetadata rootMetadata = metadata.getNodesMetadata().get(metadata.getRootNodeId()); + NodeMetadata nextNodeMetadata = metadata + .getNodesMetadata().get(rootMetadata.getTransitions().get("customTransition")); + + Map actions = new ActionNodeFactoryConfig(options).getActions(); + assertActionNodeMetadata(rootMetadata.getOperation(), "A", actions.get("A").getConfig()); + assertActionNodeMetadata(nextNodeMetadata.getOperation(), "B", actions.get("B").getConfig()); } @Test @DisplayName("Expect graph with nested composite nodes") void expectNestedCompositeNodesGraph(Vertx vertx) { // given - JsonObject options = createActionNodeConfig("A", SUCCESS_TRANSITION); + JsonObject options = actionNodeFactoryConfig("A"); - GraphNodeOptions graph = new GraphNodeOptions( + GraphNodeOptions rootNodeOptions = new GraphNodeOptions( subTasks( new GraphNodeOptions(subTasks(new GraphNodeOptions("A", NO_TRANSITIONS)), NO_TRANSITIONS)), @@ -250,68 +314,112 @@ void expectNestedCompositeNodesGraph(Vertx vertx) { ); // when - Task task = getTask(graph, options, vertx); + TaskWithMetadata taskWithMetadata = getTaskWithMetadata(rootNodeOptions, options, vertx); // then - assertEquals(TASK_NAME, task.getName()); - assertTrue(task.getRootNode().isPresent()); - Node rootNode = task.getRootNode().get(); + assertEquals(TASK_NAME, taskWithMetadata.getTask().getName()); + assertTrue(taskWithMetadata.getTask().getRootNode().isPresent()); + Node rootNode = taskWithMetadata.getTask().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); + @Test + @DisplayName("Expect metadata for graph with nested composite nodes") + void expectMetadataForNestedCompositeNodesGraph(Vertx vertx) { + // given + JsonObject options = infoLevel(actionNodeFactoryConfig("A")); + + GraphNodeOptions rootNodeOptions = new GraphNodeOptions( + subTasks( + new GraphNodeOptions(subTasks(new GraphNodeOptions("A", NO_TRANSITIONS)), + NO_TRANSITIONS)), + NO_TRANSITIONS + ); + + // when + TaskMetadata metadata = getTaskWithMetadata(rootNodeOptions, options, vertx).getMetadata(); + + Map actions = new ActionNodeFactoryConfig(options).getActions(); + + NodeMetadata rootMetadata = metadata.getNodesMetadata().get(metadata.getRootNodeId()); + NodeMetadata nestedNodeMetadata = metadata.getNodesMetadata() + .get(rootMetadata.getNestedNodes().get(0)); + NodeMetadata doubleNestedNodeMetadata = metadata.getNodesMetadata() + .get(nestedNodeMetadata.getNestedNodes().get(0)); + + assertEquals(NodeType.COMPOSITE, rootMetadata.getType()); + assertEquals(NodeType.COMPOSITE, nestedNodeMetadata.getType()); + assertEquals(NodeType.SINGLE, doubleNestedNodeMetadata.getType()); + + assertActionNodeMetadata(doubleNestedNodeMetadata.getOperation(), "A", + actions.get("A").getConfig()); + } + + private TaskWithMetadata getTaskWithMetadata(GraphNodeOptions graph, JsonObject actionNodeConfig, + Vertx vertx) { + DefaultTaskFactoryConfig taskFactoryConfig = taskFactoryConfig(graph, actionNodeConfig); return new DefaultTaskFactory().configure(taskFactoryConfig.toJson(), vertx) - .newInstance(SAMPLE_FRAGMENT_EVENT); + .newInstanceWithMetadata(fragmentEvent); + } + + private DefaultTaskFactoryConfig taskFactoryConfig() { + return taskFactoryConfig(null, null); } - private DefaultTaskFactoryConfig emptyFactoryConfig() { - return createTaskFactoryConfig(null, null); + private DefaultTaskFactoryConfig taskFactoryConfig(GraphNodeOptions rootNodeOptions) { + return taskFactoryConfig(rootNodeOptions, new JsonObject()); } - private DefaultTaskFactoryConfig createTaskFactoryConfig(GraphNodeOptions graph, - JsonObject actionNodeConfig) { + private DefaultTaskFactoryConfig taskFactoryConfig(GraphNodeOptions rootNodeOptions, + JsonObject actionNodeFactoryConfig) { DefaultTaskFactoryConfig taskFactoryConfig = new DefaultTaskFactoryConfig(); - if (graph != null) { + if (rootNodeOptions != null) { taskFactoryConfig - .setTasks(Collections.singletonMap(TASK_NAME, graph)); + .setTasks(singletonMap(TASK_NAME, rootNodeOptions)); } - if (actionNodeConfig != null) { - List nodeFactories = Arrays.asList( - new NodeFactoryOptions().setFactory(ActionNodeFactory.NAME).setConfig(actionNodeConfig), + if (actionNodeFactoryConfig != null) { + List nodeFactories = asList( + new NodeFactoryOptions().setFactory(ActionNodeFactory.NAME) + .setConfig(actionNodeFactoryConfig), 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 actionNodeFactoryConfig(String... actionNames) { + Map actionToOptions = new HashMap<>(); + asList(actionNames).forEach(actionName -> actionToOptions.put(actionName, + new ActionFactoryOptions(TEST_ACTION_FACTORY) + .setConfig(new JsonObject().put("transition", SUCCESS_TRANSITION)))); + return new ActionNodeFactoryConfig(actionToOptions).toJson(); } - private JsonObject merge(JsonObject current, String actionName, String transition) { - JsonObject newOptions = createActionNodeConfig(actionName, transition); - return current.getJsonObject("actions").mergeIn(newOptions.getJsonObject("actions")); + private JsonObject infoLevel(JsonObject nodeFactoryConfig) { + ActionNodeFactoryConfig original = new ActionNodeFactoryConfig(nodeFactoryConfig); + return new ActionNodeFactoryConfig(original.getActions(), ActionLogLevel.INFO).toJson(); } private List subTasks(GraphNodeOptions... nodes) { - return Arrays.asList(nodes); + return asList(nodes); + } + + private void assertActionNodeMetadata(OperationMetadata operation, String actionName, + JsonObject actionConfig) { + assertEquals("action", operation.getFactory()); + assertEquals(actionName, operation.getData().getString("alias")); + assertEquals(TEST_ACTION_FACTORY, operation.getData().getString("actionFactory")); + assertEquals(actionConfig, operation.getData().getJsonObject("actionConfig")); } } 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 index 054b5ff4..273bc706 100644 --- 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 @@ -16,7 +16,9 @@ package io.knotx.fragments.task.factory; import static io.knotx.fragments.HoconLoader.verify; -import static org.junit.jupiter.api.Assertions.*; +import static io.knotx.fragments.engine.api.node.single.FragmentResult.SUCCESS_TRANSITION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import io.knotx.fragments.task.factory.node.action.ActionNodeConfig; import io.knotx.fragments.task.factory.node.action.ActionNodeFactory; @@ -75,10 +77,10 @@ void expectDefaultGlobalLogLevel(Vertx vertx) throws Throwable { void expectTransitionSuccessWithNodeBThenNodeC(Vertx vertx) throws Throwable { verify("task/factory/taskWithTransitions.conf", config -> { GraphNodeOptions graphNodeOptions = new GraphNodeOptions(config); - Optional nodeB = graphNodeOptions.get("_success"); + Optional nodeB = graphNodeOptions.get(SUCCESS_TRANSITION); assertTrue(nodeB.isPresent()); assertEquals("b", getAction(nodeB.get())); - Optional nodeC = nodeB.get().get("_success"); + Optional nodeC = nodeB.get().get(SUCCESS_TRANSITION); assertTrue(nodeC.isPresent()); assertEquals("c", getAction(nodeC.get())); }, vertx); 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 index c1cd3ebe..29511750 100644 --- 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 @@ -15,22 +15,46 @@ */ package io.knotx.fragments.task.factory.node.action; +import static io.knotx.fragments.engine.api.node.single.FragmentResult.ERROR_TRANSITION; import static io.knotx.fragments.engine.api.node.single.FragmentResult.SUCCESS_TRANSITION; +import static io.knotx.fragments.task.factory.node.action.ActionNodeFactory.METADATA_ACTION_CONFIG; +import static io.knotx.fragments.task.factory.node.action.ActionNodeFactory.METADATA_ACTION_FACTORY; +import static io.knotx.fragments.task.factory.node.action.ActionNodeFactory.METADATA_ALIAS; +import static io.knotx.fragments.task.factory.node.action.ActionNodeFactory.NAME; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import io.knotx.fragments.api.Fragment; +import io.knotx.fragments.engine.NodeMetadata; import io.knotx.fragments.engine.api.node.Node; +import io.knotx.fragments.engine.api.node.NodeType; +import io.knotx.fragments.engine.api.node.single.FragmentContext; +import io.knotx.fragments.engine.api.node.single.FragmentResult; import io.knotx.fragments.engine.api.node.single.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.knotx.fragments.task.factory.NodeProvider; +import io.knotx.fragments.task.factory.node.NodeOptions; +import io.knotx.fragments.task.factory.node.StubNode; +import io.knotx.server.api.context.ClientRequest; +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 java.util.AbstractMap; import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -38,51 +62,105 @@ @ExtendWith(VertxExtension.class) class ActionNodeFactoryTest { + // ACTION NODE private static final Map NO_TRANSITIONS = Collections.emptyMap(); + private static final Map NO_EDGES = Collections.emptyMap(); + + // ACTION + private static final String ACTION_FACTORY_NAME = "test-action"; + private static final JsonObject ACTION_CONFIG = new JsonObject() + .put("actionConfigKey", "actionConfigValue"); + + private static NodeProvider emptyNodeProvider; + + @BeforeEach + void init() { + emptyNodeProvider = mock(NodeProvider.class); + when(emptyNodeProvider.initNode(any(), any())).thenThrow(new IllegalStateException()); + } @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); + ActionNodeFactoryConfig factoryConfig = new ActionNodeFactoryConfig(Collections.emptyMap()); + NodeOptions nodeOptions = nodeOptions("A"); // when, then Assertions.assertThrows( - ActionNotFoundException.class, () -> new ActionNodeFactory().configure(config, vertx) - .initNode(graph, Collections.emptyMap(), null)); + ActionNotFoundException.class, + () -> new ActionNodeFactory().configure(factoryConfig.toJson(), vertx) + .initNode(nodeOptions, NO_EDGES, emptyNodeProvider, emptyMetadata())); } @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); + ActionNodeFactoryConfig factoryConfig = factoryConfig("otherAction"); + NodeOptions nodeOptions = nodeOptions("A"); // when, then Assertions.assertThrows( - ActionNotFoundException.class, () -> new ActionNodeFactory().configure(config, vertx) - .initNode(graph, Collections.emptyMap(), null)); + ActionNotFoundException.class, + () -> new ActionNodeFactory().configure(factoryConfig.toJson(), vertx) + .initNode(nodeOptions, NO_EDGES, emptyNodeProvider, emptyMetadata())); } @Test - @DisplayName("Expect A node when action node defined.") + @DisplayName("Expect single node when action node defined.") void expectSingleActionNode(Vertx vertx) { // given String actionAlias = "A"; - JsonObject config = createNodeConfig(actionAlias, SUCCESS_TRANSITION); + ActionNodeFactoryConfig factoryConfig = factoryConfig(actionAlias); + NodeOptions nodeOptions = nodeOptions(actionAlias); + + // when + Node node = new ActionNodeFactory().configure(factoryConfig.toJson(), vertx) + .initNode(nodeOptions, NO_EDGES, emptyNodeProvider, emptyMetadata()); + + // then + assertNotNull(node); + assertTrue(node instanceof SingleNode); + assertEquals(NodeType.SINGLE, node.getType()); + } + + @Test + @Deprecated + @DisplayName("Expect single node when action node defined.") + void validateDeprecatedApi(Vertx vertx) { + // given + String actionAlias = "A"; + ActionNodeFactoryConfig factoryConfig = factoryConfig(actionAlias); GraphNodeOptions graph = new GraphNodeOptions(actionAlias, NO_TRANSITIONS); // when - Node node = new ActionNodeFactory().configure(config, vertx) - .initNode(graph, Collections.emptyMap(), null); + Node node = new ActionNodeFactory().configure(factoryConfig.toJson(), vertx) + .initNode(graph, NO_EDGES, emptyNodeProvider); // then - assertEquals(actionAlias, node.getId()); + assertNotNull(node); assertTrue(node instanceof SingleNode); + assertEquals(NodeType.SINGLE, node.getType()); + } + + @Test + @DisplayName("Expect action node id is unique.") + void expectUniqueActionNodeId(Vertx vertx) { + // given + String actionAlias = "A"; + ActionNodeFactoryConfig factoryConfig = factoryConfig(actionAlias); + NodeOptions nodeOptions = nodeOptions(actionAlias); + + ActionNodeFactory tested = new ActionNodeFactory().configure(factoryConfig.toJson(), vertx); + + // when + Node first = tested + .initNode(nodeOptions, Collections.emptyMap(), emptyNodeProvider, emptyMetadata()); + Node second = tested.initNode(nodeOptions, NO_EDGES, emptyNodeProvider, emptyMetadata()); + + // then + assertNotEquals(first.getId(), second.getId()); } @Test @@ -90,26 +168,109 @@ void expectSingleActionNode(Vertx vertx) { 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()); + ActionNodeFactoryConfig factoryConfig = factoryConfig(actionAlias); + NodeOptions nodeOptions = nodeOptions(actionAlias); + + Map edges = Stream.of( + new AbstractMap.SimpleImmutableEntry<>(SUCCESS_TRANSITION, new StubNode("B")), + new AbstractMap.SimpleImmutableEntry<>(ERROR_TRANSITION, new StubNode("C")), + new AbstractMap.SimpleImmutableEntry<>("custom", new StubNode("D"))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + // when + Node node = new ActionNodeFactory().configure(factoryConfig.toJson(), vertx) + .initNode(nodeOptions, edges, emptyNodeProvider, emptyMetadata()); + + // then + assertTrue(node.next(SUCCESS_TRANSITION).isPresent()); + assertTrue(node.next(ERROR_TRANSITION).isPresent()); + assertTrue(node.next("custom").isPresent()); + assertEquals("B", node.next(SUCCESS_TRANSITION).get().getId()); + assertEquals("C", node.next(ERROR_TRANSITION).get().getId()); + assertEquals("D", node.next("custom").get().getId()); + } + + @Test + @DisplayName("Expect action logic is applied.") + void expectActionLogicIsApplied(Vertx vertx, VertxTestContext testContext) { + // given + String actionAlias = "A"; + ActionNodeFactoryConfig factoryConfig = factoryConfig(actionAlias); + NodeOptions nodeOptions = nodeOptions(actionAlias); + + // when + Node node = new ActionNodeFactory().configure(factoryConfig.toJson(), vertx) + .initNode(nodeOptions, NO_EDGES, emptyNodeProvider, emptyMetadata()); + + // then + SingleNode singleNode = (SingleNode) node; + Single result = singleNode.execute( + new FragmentContext(new Fragment("type", new JsonObject(), "body"), new ClientRequest())); + result + .doOnSuccess(response -> testContext.verify(() -> { + assertEquals(SUCCESS_TRANSITION, response.getTransition()); + testContext.completeNow(); + })) + .doOnError(testContext::failNow) + .subscribe(); + } + + @Test + @DisplayName("Expect metadata to have correct information.") + void expectMetadata(Vertx vertx) { + // given + String actionAlias = "A"; + ActionNodeFactoryConfig factoryConfig = factoryConfig(actionAlias); + NodeOptions nodeOptions = nodeOptions(actionAlias); + + Map nodesMetadata = new HashMap<>(); + Map edges = Stream.of( + new AbstractMap.SimpleImmutableEntry<>(SUCCESS_TRANSITION, new StubNode("B")), + new AbstractMap.SimpleImmutableEntry<>(ERROR_TRANSITION, new StubNode("C")), + new AbstractMap.SimpleImmutableEntry<>("custom", new StubNode("D"))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); // when - Node node = new ActionNodeFactory().configure(config, vertx) - .initNode(graph, Collections.singletonMap(transition, new StubNode("B")), null); + Node node = new ActionNodeFactory().configure(factoryConfig.toJson(), vertx) + .initNode(nodeOptions, edges, emptyNodeProvider, nodesMetadata); // then - Optional nextNode = node.next(transition); - assertTrue(nextNode.isPresent()); - assertEquals("B", nextNode.get().getId()); + Map expectedTransitions = Stream.of(new String[][]{ + {SUCCESS_TRANSITION, "B"}, + {ERROR_TRANSITION, "C"}, + {"custom", "D"} + }).collect(Collectors.toMap(data -> data[0], data -> data[1])); + + assertEquals(1, nodesMetadata.size()); + assertTrue(nodesMetadata.values().stream().findFirst().isPresent()); + + NodeMetadata metadata = nodesMetadata.values().stream().findFirst().get(); + assertEquals(node.getId(), metadata.getNodeId()); + assertEquals(actionAlias, metadata.getLabel()); + assertEquals(node.getType(), metadata.getType()); + assertEquals(expectedTransitions, metadata.getTransitions()); + assertEquals(Collections.emptyList(), metadata.getNestedNodes()); + assertNotNull(metadata.getOperation()); + + assertEquals(NAME, metadata.getOperation().getFactory()); + JsonObject data = metadata.getOperation().getData(); + assertEquals(actionAlias, data.getString(METADATA_ALIAS)); + assertEquals(ACTION_FACTORY_NAME, data.getString(METADATA_ACTION_FACTORY)); + assertEquals(ACTION_CONFIG, data.getJsonObject(METADATA_ACTION_CONFIG)); } - private JsonObject createNodeConfig(String actionName, String transition) { + private ActionNodeFactoryConfig factoryConfig(String actionName) { return new ActionNodeFactoryConfig(Collections.singletonMap(actionName, new ActionFactoryOptions(new JsonObject()) - .setFactory("test-action") - .setConfig(new JsonObject().put("transition", transition)))) - .toJson(); + .setFactory(ACTION_FACTORY_NAME) + .setConfig(ACTION_CONFIG))); + } + + private NodeOptions nodeOptions(String actionAlias) { + return new NodeOptions(NAME, new ActionNodeConfig(actionAlias).toJson()); + } + + private Map emptyMetadata() { + return new HashMap<>(); } -} \ 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 index 8867b89f..28d28740 100644 --- 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 @@ -17,30 +17,39 @@ import static io.knotx.fragments.engine.api.node.single.FragmentResult.ERROR_TRANSITION; import static io.knotx.fragments.engine.api.node.single.FragmentResult.SUCCESS_TRANSITION; +import static io.knotx.fragments.task.factory.node.subtasks.SubtasksNodeFactory.NAME; +import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; 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.api.node.composite.CompositeNode; +import io.knotx.fragments.engine.NodeMetadata; import io.knotx.fragments.engine.api.node.Node; +import io.knotx.fragments.engine.api.node.NodeType; +import io.knotx.fragments.engine.api.node.composite.CompositeNode; +import io.knotx.fragments.task.exception.NodeFactoryNotFoundException; +import io.knotx.fragments.task.factory.GraphNodeOptions; 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.AbstractMap; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -48,63 +57,127 @@ @ExtendWith(VertxExtension.class) class SubtasksNodeFactoryTest { + private static final String FACTORY_NAME = "factoryName"; + private static final JsonObject FACTORY_CONFIG = new JsonObject(); + private static final Map NO_TRANSITIONS = Collections.emptyMap(); + private static final Map NO_EDGES = Collections.emptyMap(); + + + @Test + @DisplayName("Node is composite.") + void expectComposite(Vertx vertx) { + // given + NodeProvider nodeProvider = mock(NodeProvider.class); + when(nodeProvider.initNode(any(), any())).thenThrow(new IllegalStateException()); + + NodeOptions nodeOptions = nodeOptions(); + + // when + Node node = new SubtasksNodeFactory().configure(FACTORY_CONFIG, vertx) + .initNode(nodeOptions, NO_EDGES, nodeProvider, emptyMetadata()); + + // then + assertEquals(NodeType.COMPOSITE, node.getType()); + assertTrue(node instanceof CompositeNode); + } + + @Test + @DisplayName("Expect composite node with no nodes.") + void expectCompositeWithNoNodes(Vertx vertx) { + // given + NodeProvider nodeProvider = mock(NodeProvider.class); + when(nodeProvider.initNode(any(), any())).thenThrow(new IllegalStateException()); + + NodeOptions nodeOptions = nodeOptions(); + + // when + Node node = new SubtasksNodeFactory().configure(FACTORY_CONFIG, vertx) + .initNode(nodeOptions, NO_EDGES, nodeProvider, emptyMetadata()); + + // then + assertTrue(((CompositeNode) node).getNodes().isEmpty()); + } + + @Test + @DisplayName("Expect composite node id is unique.") + void expectUniqueNodeId(Vertx vertx) { + // given + NodeProvider nodeProvider = mock(NodeProvider.class); + when(nodeProvider.initNode(any(), any())).thenThrow(new IllegalStateException()); + NodeOptions nodeOptions = nodeOptions(); + + SubtasksNodeFactory tested = new SubtasksNodeFactory().configure(FACTORY_CONFIG, vertx); + + // when + Node first = tested.initNode(nodeOptions, NO_EDGES, nodeProvider, emptyMetadata()); + Node second = tested.initNode(nodeOptions, NO_EDGES, nodeProvider, emptyMetadata()); + + // then + assertNotEquals(first.getId(), second.getId()); + } @Test - @DisplayName("Expect composite node with single subgraph.") - void expectCompositeNodeW(Vertx vertx) { + @DisplayName("Expect exception when subtask can not be initialized.") + void expectExceptionWhenSubtaskNotInitialized(Vertx vertx) { // given + NodeProvider nodeProvider = mock(NodeProvider.class); + when(nodeProvider.initNode(any(), any())) + .thenThrow(new NodeFactoryNotFoundException(FACTORY_NAME)); + GraphNodeOptions subNodeConfig = new GraphNodeOptions( - new NodeOptions("someFactory", new JsonObject()), + new NodeOptions(FACTORY_NAME, new JsonObject()), NO_TRANSITIONS ); + NodeOptions nodeOptions = new NodeOptions(NAME, + new SubtasksNodeConfig(subTasks(subNodeConfig)).toJson()); + + // when, then + assertThrows(NodeFactoryNotFoundException.class, + () -> new SubtasksNodeFactory().configure(FACTORY_CONFIG, vertx) + .initNode(nodeOptions, NO_EDGES, nodeProvider, emptyMetadata())); + } - GraphNodeOptions graph = new GraphNodeOptions( - subTasks(subNodeConfig), + @Test + @DisplayName("Expect composite with nodes.") + void expectCompositeWithNodes(Vertx vertx) { + // given + GraphNodeOptions subNodeConfig = new GraphNodeOptions( + new NodeOptions(FACTORY_NAME, new JsonObject()), NO_TRANSITIONS ); - NodeProvider nodeProvider = mock(NodeProvider.class); - when(nodeProvider.initNode(eq(subNodeConfig))).thenReturn(new StubNode("A")); + when(nodeProvider.initNode(eq(subNodeConfig), any())).thenReturn(new StubNode("A")); + + NodeOptions nodeOptions = new NodeOptions(NAME, + new SubtasksNodeConfig(subTasks(subNodeConfig)).toJson()); // when - Node node = new SubtasksNodeFactory().configure(new JsonObject(), vertx) - .initNode(graph, Collections.emptyMap(), nodeProvider); + Node node = new SubtasksNodeFactory().configure(FACTORY_CONFIG, vertx) + .initNode(nodeOptions, Collections.emptyMap(), nodeProvider, new HashMap<>()); // 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()); + assertEquals("A", compositeRootNode.getNodes().get(0).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")); + when(nodeProvider.initNode(any(), any())).thenThrow(new IllegalStateException()); Map transitionsToNodes = new HashMap<>(); transitionsToNodes.put(SUCCESS_TRANSITION, new StubNode("B")); transitionsToNodes.put(ERROR_TRANSITION, new StubNode("C")); transitionsToNodes.put("otherTransition", new StubNode("D")); + NodeOptions nodeOptions = nodeOptions(); + // when - Node node = new SubtasksNodeFactory().configure(new JsonObject(), vertx) - .initNode(graph, transitionsToNodes, nodeProvider); + Node node = new SubtasksNodeFactory().configure(FACTORY_CONFIG, vertx) + .initNode(nodeOptions, transitionsToNodes, nodeProvider, emptyMetadata()); // then assertTrue(node.next(SUCCESS_TRANSITION).isPresent()); @@ -115,32 +188,57 @@ void expectOnlySuccessAndErrorTransitions(Vertx vertx) { } @Test - @DisplayName("Expect graph with nested composite nodes") - void expectNestedCompositeNodesGraph(Vertx vertx) { + @DisplayName("Expect metadata.") + void expectMetadata(Vertx vertx) { // given - GraphNodeOptions nestedNodeConfig = new GraphNodeOptions( - subTasks( - new GraphNodeOptions(new JsonObject()) - ), NO_TRANSITIONS); - - GraphNodeOptions graph = new GraphNodeOptions( - subTasks( - nestedNodeConfig - ), NO_TRANSITIONS + GraphNodeOptions subNodeConfig = new GraphNodeOptions( + new NodeOptions(FACTORY_NAME, new JsonObject()), + NO_TRANSITIONS ); - NodeProvider nodeProvider = mock(NodeProvider.class); + when(nodeProvider.initNode(eq(subNodeConfig), any())).thenReturn(new StubNode("A")); + + NodeOptions nodeOptions = new NodeOptions(NAME, + new SubtasksNodeConfig(subTasks(subNodeConfig)).toJson()); + Map edges = Stream.of( + new AbstractMap.SimpleImmutableEntry<>(SUCCESS_TRANSITION, new StubNode("B")), + new AbstractMap.SimpleImmutableEntry<>(ERROR_TRANSITION, new StubNode("C")), + new AbstractMap.SimpleImmutableEntry<>("custom", new StubNode("D"))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); // when - new SubtasksNodeFactory().configure(new JsonObject(), vertx) - .initNode(graph, Collections.emptyMap(), nodeProvider); + Map nodesMetadata = emptyMetadata(); + Node node = new SubtasksNodeFactory().configure(FACTORY_CONFIG, vertx) + .initNode(nodeOptions, edges, nodeProvider, nodesMetadata); // then - verify(nodeProvider, times(1)).initNode(eq(nestedNodeConfig)); + Map expectedTransitions = Stream.of(new String[][]{ + {SUCCESS_TRANSITION, "B"}, + {ERROR_TRANSITION, "C"}, + {"custom", "D"} + }).collect(Collectors.toMap(data -> data[0], data -> data[1])); + + assertTrue(nodesMetadata.values().stream().findFirst().isPresent()); + NodeMetadata metadata = nodesMetadata.values().stream().findFirst().get(); + assertEquals(node.getId(), metadata.getNodeId()); + assertEquals("composite", metadata.getLabel()); + assertEquals(node.getType(), metadata.getType()); + assertEquals(expectedTransitions, metadata.getTransitions()); + assertEquals(singletonList("A"), metadata.getNestedNodes()); + assertNotNull(metadata.getOperation()); + assertEquals(NAME, metadata.getOperation().getFactory()); + } + private NodeOptions nodeOptions() { + return new NodeOptions(NAME, + new SubtasksNodeConfig(Collections.emptyList()).toJson()); } private List subTasks(GraphNodeOptions... nodes) { return Arrays.asList(nodes); } -} \ No newline at end of file + + private Map emptyMetadata() { + return new HashMap<>(); + } +} diff --git a/handler/engine/core/src/main/java/io/knotx/fragments/engine/EventLog.java b/handler/engine/core/src/main/java/io/knotx/fragments/engine/EventLog.java index e3ac0a30..c8581edf 100644 --- a/handler/engine/core/src/main/java/io/knotx/fragments/engine/EventLog.java +++ b/handler/engine/core/src/main/java/io/knotx/fragments/engine/EventLog.java @@ -28,11 +28,11 @@ public class EventLog { private final List operations; - EventLog() { + public EventLog() { operations = new ArrayList<>(); } - EventLog(JsonObject json) { + public EventLog(JsonObject json) { operations = json.getJsonArray(OPERATIONS_KEY).stream() .map(JsonObject.class::cast) .map(EventLogEntry::new) @@ -50,10 +50,28 @@ void append(EventLogEntry logEntry) { operations.add(logEntry); } - public void appendAll(EventLog log) { + void appendAll(EventLog log) { this.operations.addAll(log.operations); } + public List getOperations() { + return new ArrayList<>(operations); + } + + public long getEarliestTimestamp() { + return operations.stream() + .mapToLong(EventLogEntry::getTimestamp) + .min() + .orElse(0); + } + + public long getLatestTimestamp() { + return operations.stream() + .mapToLong(EventLogEntry::getTimestamp) + .max() + .orElse(0); + } + public JsonObject getLog() { return toJson(); } @@ -75,7 +93,6 @@ public int hashCode() { return Objects.hash(operations); } - @Override public String toString() { return "EventLog{" + diff --git a/handler/engine/core/src/main/java/io/knotx/fragments/engine/EventLogEntry.java b/handler/engine/core/src/main/java/io/knotx/fragments/engine/EventLogEntry.java index c2513a52..b8f765ea 100644 --- a/handler/engine/core/src/main/java/io/knotx/fragments/engine/EventLogEntry.java +++ b/handler/engine/core/src/main/java/io/knotx/fragments/engine/EventLogEntry.java @@ -52,6 +52,10 @@ public static EventLogEntry error(String task, String node, String transition) { return new EventLogEntry(task, node, NodeStatus.ERROR, transition,null); } + public static EventLogEntry unprocessed(String task, String node) { + return new EventLogEntry(task, node, NodeStatus.UNPROCESSED, null, null); + } + public static EventLogEntry timeout(String task, String node) { return new EventLogEntry(task, node, NodeStatus.TIMEOUT, null, null); } @@ -74,7 +78,7 @@ private EventLogEntry(String task, String node, NodeStatus status, String transi this.nodeLog = json.getJsonObject(NODE_LOG_KEY); } - JsonObject toJson() { + public JsonObject toJson() { return new JsonObject() .put(TASK_KEY, task) .put(NODE_KEY, node) @@ -96,11 +100,36 @@ public String toString() { '}'; } - enum NodeStatus { + public String getTask() { + return task; + } + + public String getNode() { + return node; + } + + public NodeStatus getStatus() { + return status; + } + + public String getTransition() { + return transition; + } + + public long getTimestamp() { + return timestamp; + } + + public JsonObject getNodeLog() { + return nodeLog; + } + + public enum NodeStatus { SUCCESS, UNSUPPORTED_TRANSITION, ERROR, - TIMEOUT //? + TIMEOUT, + UNPROCESSED } } diff --git a/handler/engine/core/src/main/java/io/knotx/fragments/engine/FragmentsEngine.java b/handler/engine/core/src/main/java/io/knotx/fragments/engine/FragmentsEngine.java index abe96fc9..3351be65 100644 --- a/handler/engine/core/src/main/java/io/knotx/fragments/engine/FragmentsEngine.java +++ b/handler/engine/core/src/main/java/io/knotx/fragments/engine/FragmentsEngine.java @@ -99,4 +99,4 @@ private List traceEngineResults(List results) { } return results; } -} \ No newline at end of file +} diff --git a/handler/engine/core/src/main/java/io/knotx/fragments/engine/TaskExecutionContext.java b/handler/engine/core/src/main/java/io/knotx/fragments/engine/TaskExecutionContext.java index ce3306f7..43cddeee 100644 --- a/handler/engine/core/src/main/java/io/knotx/fragments/engine/TaskExecutionContext.java +++ b/handler/engine/core/src/main/java/io/knotx/fragments/engine/TaskExecutionContext.java @@ -79,10 +79,11 @@ SingleSource handleError(Throwable error) { handleRegularError(error); } FragmentEvent fragmentEvent = fragmentEventContext.getFragmentEvent(); - return Single.just(new FragmentResult(fragmentEvent.getFragment(), ERROR_TRANSITION, prepareErrorActionLog(error))); + return Single.just(new FragmentResult(fragmentEvent.getFragment(), ERROR_TRANSITION, + prepareErrorActionLog(error))); } - private JsonObject prepareErrorActionLog(Throwable error){ + private JsonObject prepareErrorActionLog(Throwable error) { return new JsonObject() .put("error", error.getMessage()); } @@ -153,8 +154,7 @@ void updateResult(FragmentResult fragmentResult) { void handleSuccess(FragmentResult fragmentResult) { FragmentEvent fragmentEvent = fragmentEventContext.getFragmentEvent(); fragmentEvent.setStatus(Status.SUCCESS); - fragmentEvent - .log(EventLogEntry.success(taskName, currentNode.getId(), fragmentResult)); + fragmentEvent.log(EventLogEntry.success(taskName, currentNode.getId(), fragmentResult)); } private EventLogEntry getEventLogEntry(Throwable error) {