Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow custom modules registration #112

Merged
merged 5 commits into from
Oct 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ endif::[]
image:https://img.shields.io/github/release/manikmagar/mulefd.svg[Release,link=https:/manikmagar/mulefd/releases]
image:https:/manikmagar/mulefd/workflows/ci-build/badge.svg[Build Status,link=https:/manikmagar/mulefd/actions]
image:https://api.dependabot.com/badges/status?host=github&repo=manikmagar/mulefd[Dependabot Status,https://dependabot.com]

image:https://sonarcloud.io/api/project_badges/measure?project=manikmagar_mulefd&metric=vulnerabilities[SonarCloud Vulnerability]
image:https://sonarcloud.io/api/project_badges/measure?project=manikmagar_mulefd&metric=alert_status[SonarCloud Quality]

toc::[]

Expand Down Expand Up @@ -276,6 +277,32 @@ Target directory to output generated diagram can be specified with `-t {director

The file name for diagram defaults to `mule-diagram.png`. This can be changed by specifying `-o {filename}` argument.

== Known modules/connectors
Mule has many connectors/modules that can be used for building flows and sub-flows, and list keeps growing. This tool maintains a list of known components with their supported operations for including in the generated diagram.

You can find this list in source code link:src/main/resources/default-mule-components.csv[].

Each record in this file has following columns -

[source, csv]
----
prefix,operation,sourceFlag,path,configName,async

----
* **prefix**: Namespace of module/connector. Eg. `vm`, `http` etc.
* **operation**: Name of an operation or input source in that namespace. Eg. `listener`, `consume` in `vm` etc. This supports wildcard entries (values defined as `*`) for non-source (`sourceFlag=false`) entries.
* **sourceFlag**: `true` if `operation` is an input source withing that namespace. Eg. `listener` in `vm`.
* **path**: Name of the attribute on operation which can help identify resource path. Eg. `queueName` for `vm:listener` or `path` for `http:request`. For sources, this is visible on source nodes in diagram.
* **configName**: Name of the module configuration. Eg. `config-ref` attribute name for `vm:listener`. For sources, this is visible on source nodes in diagram. This can help identify source uniquely when multiple configuration exists.
* **async**: `true` if this is an asynchronous operation. Defaults to `false`.

=== Registering custom modules
If any module is missing in the default list - either being a new module or a custom module, then it is possible for users to register their modules.

If `mulefd-components.csv` named CSV file exists in `sourcePath`, then all modules/connectors within that file are registered as known components. Structure of this file must be same as default components file explained above.

See example file at link:src/test/resources/mulefd-components.csv[].

== Copyright & License

Licensed under the MIT License, see the link:LICENSE[LICENSE] file for details.
50 changes: 35 additions & 15 deletions src/main/java/com/javastreets/mulefd/DiagramRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
public class DiagramRenderer {
public static final int MULE_VERSION_4 = 4;
public static final int MULE_VERSION_3 = 3;
public static final String DEFAULT_MULE_COMPONENTS_CSV = "default-mule-components.csv";
public static final String MULE_CUSTOM_COMPONENTS_CSV = "mulefd-components.csv";
Logger log = LoggerFactory.getLogger(DiagramRenderer.class);

private CommandModel commandModel;
Expand All @@ -34,8 +36,30 @@ public DiagramRenderer(CommandModel commandModel) {

Map<String, ComponentItem> prepareKnownComponents() {
Map<String, ComponentItem> items = new HashMap<>();
try (BufferedReader br = new BufferedReader(new InputStreamReader(Thread.currentThread()
.getContextClassLoader().getResourceAsStream("mule-components.csv")))) {
try {
loadComponentsFile(items, new InputStreamReader(Objects.requireNonNull(Thread.currentThread()
.getContextClassLoader().getResourceAsStream(DEFAULT_MULE_COMPONENTS_CSV))));
} catch (IOException e) {
log.error("Error while loading default mule components file.", e);
}
Path mulefdComponents = this.commandModel.getSourcePath().toFile().isDirectory()
? this.commandModel.getSourcePath().resolve(MULE_CUSTOM_COMPONENTS_CSV)
: this.commandModel.getSourcePath().resolveSibling(MULE_CUSTOM_COMPONENTS_CSV);
if (Files.exists(mulefdComponents)) {
try {
log.info("Found {}. This will be loaded to register any custom modules/connectors.",
mulefdComponents);
loadComponentsFile(items, new InputStreamReader(Files.newInputStream(mulefdComponents)));
} catch (IOException e) {
throw new DrawingException("Unable to load file - " + MULE_CUSTOM_COMPONENTS_CSV, e);
}
}
return items;
}

private void loadComponentsFile(Map<String, ComponentItem> items, InputStreamReader reader)
throws IOException {
try (BufferedReader br = new BufferedReader(reader)) {
for (String line; (line = br.readLine()) != null;) {
if (!line.startsWith("prefix")) {
log.debug("Reading component line - {}", line);
Expand Down Expand Up @@ -63,20 +87,19 @@ Map<String, ComponentItem> prepareKnownComponents() {
}
}
// line is not visible here.
} catch (IOException e) {
log.error("mule-components file not found", e);
}
return items;
}

public Boolean render() {
try {
List<FlowContainer> flows = findFlows();
DrawingContext context = drawingContext(commandModel);
List<FlowContainer> flows = findFlows(context);
context.setComponents(Collections.unmodifiableList(flows));
if (commandModel.getFlowName() != null && flows.stream().noneMatch(
flowContainer -> flowContainer.getName().equalsIgnoreCase(commandModel.getFlowName()))) {
throw new DrawingException("Flow not found - " + commandModel.getFlowName());
}
return diagram(flows);
return diagram(context);
} catch (IOException e) {
log.error("Error while parsing xml file", e);
return false;
Expand All @@ -87,17 +110,16 @@ boolean existInSource(String path) {
return Files.exists(Paths.get(commandModel.getSourcePath().toString(), path));
}

List<FlowContainer> findFlows() throws IOException {
List<FlowContainer> findFlows(DrawingContext context) throws IOException {
Path newSourcePath = getMuleSourcePath();
List<FlowContainer> flows = new ArrayList<>();
try (Stream<Path> paths = Files.walk(newSourcePath)) {
List<Path> xmls = paths
.filter(
path -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".xml"))
.collect(Collectors.toList());
Map<String, ComponentItem> knownComponents = prepareKnownComponents();
for (Path path : xmls) {
flows(flows, knownComponents, path);
flows(flows, context.getKnownComponents(), path);
}
}
return flows;
Expand Down Expand Up @@ -142,14 +164,11 @@ void flows(List<FlowContainer> flows, Map<String, ComponentItem> knownComponents
}
}

Boolean diagram(List<FlowContainer> flows) {
if (flows.isEmpty()) {
Boolean diagram(DrawingContext context) {
if (context.getComponents().isEmpty()) {
log.warn("No mule flows found for creating diagram.");
return false;
}
DrawingContext context = drawingContext(commandModel);
context.setComponents(Collections.unmodifiableList(flows));
context.setKnownComponents(prepareKnownComponents());
ServiceLoader<Diagram> diagramServices = ServiceLoader.load(Diagram.class);
Iterator<Diagram> its = diagramServices.iterator();
boolean drawn = false;
Expand Down Expand Up @@ -178,6 +197,7 @@ public DrawingContext drawingContext(CommandModel model) {
context.setOutputFile(new File(model.getTargetPath().toFile(), model.getOutputFilename()));
context.setFlowName(model.getFlowName());
context.setGenerateSingles(model.isGenerateSingles());
context.setKnownComponents(prepareKnownComponents());
return context;
}
}
38 changes: 24 additions & 14 deletions src/test/java/com/javastreets/mulefd/DiagramRendererTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
import static org.mockito.Mockito.*;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.nio.file.*;
import java.util.Collections;
import java.util.List;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand All @@ -22,7 +21,7 @@
import com.javastreets.mulefd.app.CommandModel;
import com.javastreets.mulefd.drawings.DiagramType;
import com.javastreets.mulefd.drawings.DrawingContext;
import com.javastreets.mulefd.model.FlowContainer;
import com.javastreets.mulefd.model.Component;

import io.github.netmikey.logunit.api.LogCapturer;

Expand All @@ -39,26 +38,27 @@ class DiagramRendererTest {
void emptySourceDirRendering() {
DiagramRenderer renderer = Mockito.spy(new DiagramRenderer(getCommandModel(tempDir.toPath())));
doReturn(Collections.emptyMap()).when(renderer).prepareKnownComponents();
doReturn(false).when(renderer).diagram(anyList());
doReturn(false).when(renderer).diagram(any(DrawingContext.class));
assertThat(renderer.render()).isFalse();
verify(renderer).prepareKnownComponents();
ArgumentCaptor<List<FlowContainer>> captor = ArgumentCaptor.forClass(List.class);
ArgumentCaptor<DrawingContext> captor = ArgumentCaptor.forClass(DrawingContext.class);
verify(renderer).diagram(captor.capture());
assertThat(captor.getValue()).isEmpty();
assertThat(captor.getValue().getComponents()).isEmpty();
}


@Test
@DisplayName("Skips rendering non-mule file")
void skipsNonMuleFileRendering() {
Path sourcePath = Paths.get("src/test/resources/renderer/non-mule");
DiagramRenderer renderer = Mockito.spy(new DiagramRenderer(getCommandModel(sourcePath)));
doReturn(Collections.emptyMap()).when(renderer).prepareKnownComponents();
doReturn(false).when(renderer).diagram(anyList());
doReturn(false).when(renderer).diagram(any(DrawingContext.class));
assertThat(renderer.render()).isFalse();
verify(renderer).prepareKnownComponents();
ArgumentCaptor<List<FlowContainer>> captor = ArgumentCaptor.forClass(List.class);
ArgumentCaptor<DrawingContext> captor = ArgumentCaptor.forClass(DrawingContext.class);
verify(renderer).diagram(captor.capture());
assertThat(captor.getValue()).isEmpty();
assertThat(captor.getValue().getComponents()).isEmpty();
logs.assertContains(
"Not a mule configuration file: " + Paths.get(sourcePath.toString(), "non-mule-file.xml"));
}
Expand All @@ -69,13 +69,13 @@ void singleFileRendering() {
DiagramRenderer renderer = Mockito
.spy(new DiagramRenderer(getCommandModel(Paths.get("src/test/resources/renderer/single"))));
doReturn(Collections.emptyMap()).when(renderer).prepareKnownComponents();
doReturn(false).when(renderer).diagram(anyList());
doReturn(false).when(renderer).diagram(any(DrawingContext.class));
assertThat(renderer.render()).isFalse();
verify(renderer).prepareKnownComponents();
ArgumentCaptor<List<FlowContainer>> captor = ArgumentCaptor.forClass(List.class);
ArgumentCaptor<DrawingContext> captor = ArgumentCaptor.forClass(DrawingContext.class);
verify(renderer).diagram(captor.capture());
assertThat(captor.getValue()).as("Flow container list").hasSize(1)
.extracting(FlowContainer::getType, FlowContainer::getName)
assertThat(captor.getValue().getComponents()).as("Flow container list").hasSize(1)
.extracting(Component::getType, Component::getName)
.containsExactly(tuple("flow", "test-hello-appFlow"));
}

Expand Down Expand Up @@ -113,6 +113,16 @@ void prepareKnownComponents() {
.isNotEmpty();
}

@Test
@DisplayName("Prepare components from mulefd csv file")
void prepareKnownComponentsWithMulefdFile() throws IOException {
Files.copy(Paths.get("src/test/resources/mulefd-components.csv"),
tempDir.toPath().resolve("mulefd-components.csv"), StandardCopyOption.REPLACE_EXISTING);
assertThat(new DiagramRenderer(getCommandModel(tempDir.toPath())).prepareKnownComponents())
.isNotEmpty().containsKey("kafka:message-listener");
Files.deleteIfExists(Paths.get("./mulefd-components.csv"));
}

@Test
@DisplayName("Single config file as source")
void sourcePathXmlFile() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;

import com.javastreets.mulefd.app.CommandModel;
import com.javastreets.mulefd.drawings.DiagramType;
import com.javastreets.mulefd.drawings.DrawingContext;
import com.javastreets.mulefd.model.FlowContainer;

public class DiagramRendererTestUtil {
Expand All @@ -26,7 +28,16 @@ public static CommandModel getCommandModel(Path sourcePath) {
public static List<FlowContainer> getFlows(Path sourcePath) throws IOException {
CommandModel commandModel = getCommandModel(sourcePath);
DiagramRenderer diagramRenderer = new DiagramRenderer(commandModel);
return diagramRenderer.findFlows();
return diagramRenderer.findFlows(diagramRenderer.drawingContext(commandModel));
}

public static DrawingContext getDrawingContext(Path sourcePath) throws IOException {
CommandModel commandModel = getCommandModel(sourcePath);
DiagramRenderer diagramRenderer = new DiagramRenderer(commandModel);
DrawingContext context = diagramRenderer.drawingContext(commandModel);
List<FlowContainer> flows = diagramRenderer.findFlows(context);
context.setComponents(Collections.unmodifiableList(flows));
return context;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -198,6 +199,30 @@ void drawToValidateGraph_SingleFlow() throws Exception {

}

@Test
@DisplayName("Validate generated graph in json for components from mulefd-components.csv file")
void drawToValidateGraph_Mulefd_Components() throws Exception {
Files.copy(Paths.get("src/test/resources/mulefd-components.csv"),
Paths.get("./mulefd-components.csv"), StandardCopyOption.REPLACE_EXISTING);

DrawingContext context = DiagramRendererTestUtil.getDrawingContext(
Paths.get("src/test/resources/kafka-flows-mulefd-components-example.xml"));

GraphDiagram graphDiagram = Mockito.spy(new GraphDiagram());
when(graphDiagram.getDiagramHeaderLines()).thenReturn(new String[] {"Test Diagram"});
graphDiagram.draw(context);
ArgumentCaptor<MutableGraph> graphArgumentCaptor = ArgumentCaptor.forClass(MutableGraph.class);
verify(graphDiagram).writGraphToFile(any(File.class), graphArgumentCaptor.capture());
MutableGraph generatedGraph = graphArgumentCaptor.getValue();
Graphviz.useEngine(new GraphvizV8Engine());
String jsonGraph = Graphviz.fromGraph(generatedGraph).render(Format.JSON).toString();
String ref = new String(Files
.readAllBytes(Paths.get("src/test/resources/kafka-flows-mulefd-components-example.json")));
JSONAssert.assertEquals(ref, jsonGraph, JSONCompareMode.STRICT);
Graphviz.releaseEngine();

}

@Test
void drawWithSinglesGeneration() {
// Get the Java runtime
Expand Down
Loading