This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push: new 5a10130c215 CAMEL-20601: Support error handler on Camel JBang bind command (#13594) 5a10130c215 is described below commit 5a10130c215ff4cdd75153a162f068838b49514c Author: Christoph Deppisch <cdeppi...@redhat.com> AuthorDate: Fri Mar 22 18:28:44 2024 +0100 CAMEL-20601: Support error handler on Camel JBang bind command (#13594) - Allow to add error handler to the Pipe resource - Support different error handler types (sink, uri, log, none) - Support user properties on the error handlers --- .../apache/camel/dsl/jbang/core/commands/Bind.java | 108 ++++- .../templates/error-handler-log.yaml.tmpl | 2 + .../templates/error-handler-sink-kamelet.yaml.tmpl | 8 + .../templates/error-handler-sink-uri.yaml.tmpl | 5 + .../templates/pipe-kamelet-kamelet.yaml.tmpl | 1 + .../resources/templates/pipe-kamelet-uri.yaml.tmpl | 1 + .../resources/templates/pipe-uri-kamelet.yaml.tmpl | 1 + .../resources/templates/pipe-uri-uri.yaml.tmpl | 1 + .../camel/dsl/jbang/core/commands/BindTest.java | 472 +++++++++++++++++++++ 9 files changed, 595 insertions(+), 4 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java index 27b16288bc6..0c6d19e41cc 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java @@ -70,6 +70,10 @@ public class Bind extends CamelCommand { required = true) String sink; + @CommandLine.Option(names = { "--error-handler" }, + description = "Add error handler (none|log|sink:<endpoint>). Sink endpoints are expected in the format \"[[apigroup/]version:]kind:[namespace/]name\", plain Camel URIs or Kamelet name.") + String errorHandler; + @CommandLine.Option(names = { "--property" }, description = "Adds a pipe property in the form of [source|sink|step-<n>].<key>=<value> where <n> is the step number starting from 1", arity = "0") @@ -157,11 +161,78 @@ public class Bind extends CamelCommand { stepsContext = sb.toString(); } + String errorHandlerContext = ""; + if (errorHandler != null) { + StringBuilder sb = new StringBuilder("\n errorHandler:\n"); + + Map<String, Object> errorHandlerParameters = getProperties("error-handler"); + + String[] errorHandlerTokens = errorHandler.split(":", 2); + String errorHandlerType = errorHandlerTokens[0]; + + String errorHandlerSpec; + switch (errorHandlerType) { + case "sink": + if (errorHandlerTokens.length != 2) { + printer().println( + "Invalid error handler syntax. Type 'sink' needs an endpoint configuration (ie sink:endpointUri)"); + return -1; + } + String endpoint = errorHandlerTokens[1]; + + String sinkType; + Map<String, Object> errorHandlerSinkProperties = getProperties("error-handler.sink"); + + // remove sink properties from error handler parameters + errorHandlerSinkProperties.keySet().stream() + .map(key -> "sink." + key) + .filter(errorHandlerParameters::containsKey) + .forEach(errorHandlerParameters::remove); + + if (endpoint.contains(":")) { + sinkType = "uri"; + if (endpoint.contains("?")) { + String query = StringHelper.after(endpoint, "?"); + endpoint = StringHelper.before(endpoint, "?"); + if (query != null) { + errorHandlerSinkProperties.putAll(URISupport.parseQuery(query, true)); + } + } + } else { + sinkType = "kamelet"; + errorHandlerSinkProperties = kameletProperties(endpoint, errorHandlerSinkProperties); + } + + is = Bind.class.getClassLoader() + .getResourceAsStream("templates/error-handler-sink-%s.yaml.tmpl".formatted(sinkType)); + errorHandlerSpec = IOHelper.loadText(is); + IOHelper.close(is); + errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.Name }}", endpoint); + errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.ErrorHandlerProperties }}", + asEndpointProperties(errorHandlerSinkProperties, 4)); + errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.ErrorHandlerParameter }}", + asErrorHandlerParameters(errorHandlerParameters)); + break; + case "log": + is = Bind.class.getClassLoader().getResourceAsStream("templates/error-handler-log.yaml.tmpl"); + errorHandlerSpec = IOHelper.loadText(is); + IOHelper.close(is); + errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.ErrorHandlerParameter }}", + asErrorHandlerParameters(errorHandlerParameters)); + break; + default: + errorHandlerSpec = " none: {}"; + } + sb.append(errorHandlerSpec); + errorHandlerContext = sb.toString(); + } + String name = FileUtil.onlyName(file, false); context = context.replaceFirst("\\{\\{ \\.Name }}", name); context = context.replaceFirst("\\{\\{ \\.Source }}", sourceEndpoint); context = context.replaceFirst("\\{\\{ \\.Sink }}", sinkEndpoint); context = context.replaceFirst("\\{\\{ \\.Steps }}", stepsContext); + context = context.replaceFirst("\\{\\{ \\.ErrorHandler }}", errorHandlerContext); Map<String, Object> sourceProperties = getProperties("source"); if ("kamelet".equals(in)) { @@ -204,6 +275,24 @@ public class Bind extends CamelCommand { return 0; } + /** + * Creates YAML snippet representing the error handler parameters section. + * + * @param props the properties to set as error handler parameters. + */ + private String asErrorHandlerParameters(Map<String, Object> props) { + if (props.isEmpty()) { + return "parameters: {}"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("parameters:\n"); + for (Map.Entry<String, Object> propertyEntry : props.entrySet()) { + sb.append(" ").append(propertyEntry.getKey()).append(": ").append(propertyEntry.getValue()).append("\n"); + } + return sb.toString().trim(); + } + /** * Creates YAML snippet representing the endpoint properties section. * @@ -211,15 +300,26 @@ public class Bind extends CamelCommand { * @return */ private String asEndpointProperties(Map<String, Object> props) { + return asEndpointProperties(props, 0); + } + + /** + * Creates YAML snippet representing the endpoint properties section. + * + * @param props the properties to set as endpoint properties. + * @param additionalIndent optional number of additional spaces used as indentation. + * @return + */ + private String asEndpointProperties(Map<String, Object> props, int additionalIndent) { StringBuilder sb = new StringBuilder(); if (props.isEmpty()) { // create a dummy placeholder, so it is easier to add new properties manually - return sb.append("#properties:\n ").append("#key: \"value\"").toString(); + return sb.append("#properties:\n ").append(" ".repeat(additionalIndent)).append("#key: \"value\"").toString(); } sb.append("properties:\n"); for (Map.Entry<String, Object> propertyEntry : props.entrySet()) { - sb.append(" ").append(propertyEntry.getKey()).append(": ") + sb.append(" ").append(" ".repeat(additionalIndent)).append(propertyEntry.getKey()).append(": ") .append(propertyEntry.getValue()).append("\n"); } return sb.toString().trim(); @@ -227,7 +327,7 @@ public class Bind extends CamelCommand { /** * Extracts properties from given property arguments. Filter properties by given prefix. This way each component in - * pipe (source, sink, step[1-n]) can have its individual properties. + * pipe (source, sink, errorHandler, step[1-n]) can have its individual properties. * * @param keyPrefix * @return @@ -240,7 +340,7 @@ public class Bind extends CamelCommand { String[] keyValue = propertyExpression.split("=", 2); if (keyValue.length != 2) { printer().printf( - "property '%s' does not follow format [source|sink|step-<n>].<key>=<value>%n", + "property '%s' does not follow format [source|sink|error-handler|step-<n>].<key>=<value>%n", propertyExpression); continue; } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-log.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-log.yaml.tmpl new file mode 100644 index 00000000000..3261b156fb0 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-log.yaml.tmpl @@ -0,0 +1,2 @@ + log: + {{ .ErrorHandlerParameter }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-kamelet.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-kamelet.yaml.tmpl new file mode 100644 index 00000000000..2b418ebb2f2 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-kamelet.yaml.tmpl @@ -0,0 +1,8 @@ + sink: + endpoint: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: {{ .Name }} + {{ .ErrorHandlerProperties }} + {{ .ErrorHandlerParameter }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-uri.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-uri.yaml.tmpl new file mode 100644 index 00000000000..ff67827b25a --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-uri.yaml.tmpl @@ -0,0 +1,5 @@ + sink: + endpoint: + uri: {{ .Name }} + {{ .ErrorHandlerProperties }} + {{ .ErrorHandlerParameter }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-kamelet.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-kamelet.yaml.tmpl index 1583542184a..b2536e72670 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-kamelet.yaml.tmpl +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-kamelet.yaml.tmpl @@ -16,3 +16,4 @@ spec: apiVersion: camel.apache.org/v1 name: {{ .Sink }} {{ .SinkProperties }} +{{ .ErrorHandler }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl index 07de5540d3f..560840b1838 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl @@ -13,3 +13,4 @@ spec: sink: uri: {{ .Sink }} {{ .SinkProperties }} +{{ .ErrorHandler }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl index 9c7b90820a7..44b3bfc296c 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl @@ -13,3 +13,4 @@ spec: apiVersion: camel.apache.org/v1 name: {{ .Sink }} {{ .SinkProperties }} +{{ .ErrorHandler }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-uri.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-uri.yaml.tmpl index b71a4e1f96a..2e0050262fa 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-uri.yaml.tmpl +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-uri.yaml.tmpl @@ -10,3 +10,4 @@ spec: sink: uri: {{ .Sink }} {{ .SinkProperties }} +{{ .ErrorHandler }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/BindTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/BindTest.java index edf952212f9..02cdc70e6e6 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/BindTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/BindTest.java @@ -567,6 +567,478 @@ class BindTest extends CamelCommandBaseTest { """.trim(), output); } + @Test + public void shouldBindKameletSinkErrorHandler() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log-sink"; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + parameters: {} + """.trim(), output); + } + + @Test + public void shouldBindKameletSinkErrorHandlerWithParameters() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log-sink"; + + command.properties = new String[] { + "error-handler.maximumRedeliveries=3", + "error-handler.redeliveryDelay=2000" + }; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + parameters: + redeliveryDelay: 2000 + maximumRedeliveries: 3 + """.trim(), output); + } + + @Test + public void shouldBindKameletSinkErrorHandlerAndSinkProperties() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log-sink"; + + command.properties = new String[] { + "error-handler.sink.showHeaders=true", + "error-handler.maximumRedeliveries=3", + "error-handler.redeliveryDelay=2000" + }; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + properties: + showHeaders: true + parameters: + redeliveryDelay: 2000 + maximumRedeliveries: 3 + """.trim(), output); + } + + @Test + public void shouldBindEndpointUriSinkErrorHandler() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log:error"; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + uri: log:error + #properties: + #key: "value" + parameters: {} + """.trim(), output); + } + + @Test + public void shouldBindEndpointUriSinkErrorHandlerWithParameters() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log:error"; + + command.properties = new String[] { + "error-handler.maximumRedeliveries=3", + "error-handler.redeliveryDelay=2000" + }; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + uri: log:error + #properties: + #key: "value" + parameters: + redeliveryDelay: 2000 + maximumRedeliveries: 3 + """.trim(), output); + } + + @Test + public void shouldBindEndpointUriSinkErrorHandlerAndSinkProperties() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log:error"; + + command.properties = new String[] { + "error-handler.sink.showHeaders=true", + "error-handler.maximumRedeliveries=3", + "error-handler.redeliveryDelay=2000" + }; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + uri: log:error + properties: + showHeaders: true + parameters: + redeliveryDelay: 2000 + maximumRedeliveries: 3 + """.trim(), output); + } + + @Test + public void shouldBindEndpointUriSinkErrorHandlerAndUriProperties() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log:error?showStreams=false"; + + command.properties = new String[] { + "error-handler.sink.showHeaders=true", + "error-handler.maximumRedeliveries=3", + "error-handler.redeliveryDelay=2000" + }; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + uri: log:error + properties: + showStreams: false + showHeaders: true + parameters: + redeliveryDelay: 2000 + maximumRedeliveries: 3 + """.trim(), output); + } + + @Test + public void shouldBindWithLogErrorHandler() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "log"; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + log: + parameters: {} + """.trim(), output); + } + + @Test + public void shouldBindWithLogErrorHandlerWithParameters() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "log"; + + command.properties = new String[] { + "error-handler.maximumRedeliveries=3", + "error-handler.redeliveryDelay=2000" + }; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + log: + parameters: + redeliveryDelay: 2000 + maximumRedeliveries: 3 + """.trim(), output); + } + + @Test + public void shouldBindWithNoErrorHandler() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "none"; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + none: {} + """.trim(), output); + } + @Test public void shouldSupportJsonOutput() throws Exception { Bind command = new Bind(new CamelJBangMain().withPrinter(printer));