This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-25381-normalize-kamelets in repository https://gitbox.apache.org/repos/asf/camel.git
commit 8998b8cb9cc54f94d08a4798ae211f1f63e5d7cf Author: Claus Ibsen <[email protected]> AuthorDate: Tue Oct 6 17:40:31 2026 +0200 CAMEL-25381: camel-jbang - camel validate normalize supports Kamelet files A Kamelet file is loaded as a route template and its spec.template is replaced by the canonical YAML DSL of that template; the rest of the file (license, metadata, definition, dependencies, comments) is kept as written. Route templates of other files are no longer dropped from the output. The YAML dump writes a kamelet: endpoint with the Kamelet in the uri (kamelet:log-sink) instead of a templateId parameter, and the YAML DSL keeps the order of map entries such as bean properties as written. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m --- .../camel/yaml/out/YamlModelWriterSupport.java | 11 +- .../core/commands/validate/KameletNormalizer.java | 268 +++++++++++++++++++++ .../commands/validate/YamlNormalizeCommand.java | 66 ++++- .../validate/YamlNormalizeKameletTest.java | 161 +++++++++++++ .../kamelets/tag-order-action.kamelet.yaml | 62 +++++ .../dsl/yaml/common/YamlDeserializerSupport.java | 6 +- .../camel/dsl/yaml/PipeLoaderErrorHandlerTest.java | 3 +- 7 files changed, 567 insertions(+), 10 deletions(-) diff --git a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java index 6af0454bb719..4cf9af451515 100644 --- a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java +++ b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java @@ -230,7 +230,16 @@ public abstract class YamlModelWriterSupport { if (idx != -1) { scheme = scheme.substring(0, idx); } - jo.put("uri", scheme); + if ("kamelet".equals(scheme) && params.get("templateId") != null) { + // the Kamelet is named in the uri (kamelet:log-sink, kamelet:source), not as a templateId + // parameter: that is how Kamelets are written and read + params = new LinkedHashMap<>(params); + String path = params.remove("templateId"); + String routeId = params.remove("routeId"); + jo.put("uri", "kamelet:" + path + (routeId != null ? "/" + routeId : "")); + } else { + jo.put("uri", scheme); + } } if (params != null && !params.isEmpty()) { JsonObject p = new JsonObject(); diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/KameletNormalizer.java b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/KameletNormalizer.java new file mode 100644 index 000000000000..910e8a3dfb45 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/KameletNormalizer.java @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.dsl.jbang.core.commands.validate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Normalizes a Kamelet file: its spec.template is replaced by the canonical YAML DSL of the route template the Kamelet + * loads as, taken from a route template dump, and the rest of the file (metadata, definition, dependencies, comments) + * is kept as written. + */ +final class KameletNormalizer { + + private static final Pattern TEMPLATE_ID = Pattern.compile("^ {4}id: \"?([^\"\\s]+)\"?\\s*$"); + + private KameletNormalizer() { + } + + /** Whether the content is a Kamelet: a document of kind Kamelet. */ + static boolean isKamelet(String content) { + return content != null && Pattern.compile("(?m)^kind:\\s*Kamelet\\s*$").matcher(content).find(); + } + + /** The name of the Kamelet: metadata.name, else the file name without .kamelet.yaml. */ + static String kameletName(String content, String fileName) { + boolean inMetadata = false; + for (String line : content.split("\n", -1)) { + if (line.startsWith("metadata:")) { + inMetadata = true; + } else if (inMetadata && !line.isBlank() && !line.startsWith(" ") && !line.startsWith("#")) { + inMetadata = false; + } else if (inMetadata) { + Matcher m = Pattern.compile("^ {2}name:\\s*\"?([^\"\\s]+)\"?\\s*$").matcher(line); + if (m.find()) { + return m.group(1); + } + } + } + String name = fileName.replace('\\', '/'); + name = name.substring(name.lastIndexOf('/') + 1); + return name.endsWith(".kamelet.yaml") ? name.substring(0, name.length() - ".kamelet.yaml".length()) : name; + } + + /** + * The Kamelet with its spec.template replaced by the beans and route of the route template of the same id in the + * dump, or null when the dump has no such template or the Kamelet has no spec.template. + */ + static String normalize(String kamelet, String dump, String templateId) { + List<String> chunk = templateChunk(dump, templateId); + if (chunk == null) { + return null; + } + String[] lines = kamelet.split("\n", -1); + int spec = -1; + for (int i = 0; i < lines.length; i++) { + if (lines[i].matches("^spec:\\s*$")) { + spec = i; + break; + } + } + if (spec < 0) { + return null; + } + // the keys of spec are at the indent of its first key; template is one of them + int template = -1; + int templateIndent = -1; + for (int i = spec + 1; i < lines.length; i++) { + String l = lines[i]; + if (l.isBlank() || l.trim().startsWith("#")) { + continue; + } + int indent = indent(l); + if (indent == 0) { + break; + } + if (templateIndent < 0) { + templateIndent = indent; + } + if (indent == templateIndent && l.trim().matches("^template:\\s*$")) { + template = i; + break; + } + } + if (template < 0) { + return null; + } + int end = lines.length; + for (int i = template + 1; i < lines.length; i++) { + String l = lines[i]; + if (l.isBlank()) { + continue; + } + if (indent(l) <= templateIndent) { + end = i; + break; + } + } + // a template written as route: keeps that key, else from: + boolean routeKey = false; + for (int i = template + 1; i < end; i++) { + if (lines[i].trim().startsWith("route:") && indent(lines[i]) > templateIndent) { + routeKey = true; + break; + } + if (lines[i].trim().startsWith("from:") && indent(lines[i]) > templateIndent) { + break; + } + } + // the children of template at the indent the file uses for them + int child = templateIndent + 2; + for (int i = template + 1; i < end; i++) { + if (!lines[i].isBlank() && !lines[i].trim().startsWith("#")) { + child = indent(lines[i]); + break; + } + } + List<String> body = new ArrayList<>(); + // the dump: "- routeTemplate:" at 0, its keys at 4 (id, parameters, beans, route), route's from at 6 + List<String> beans = block(chunk, "beans:", 4); + if (beans != null) { + body.addAll(shift(beans, child - 4)); + } + List<String> route = block(chunk, "route:", 4); + if (route == null) { + return null; + } + if (routeKey) { + body.addAll(shift(route, child - 4)); + } else { + List<String> from = block(route, "from:", 6); + if (from == null) { + return null; + } + body.addAll(shift(from, child - 6)); + } + // trailing blank lines of the template block belong to what follows it + int bodyEnd = end; + while (bodyEnd > template + 1 && lines[bodyEnd - 1].isBlank()) { + bodyEnd--; + } + List<String> out = new ArrayList<>(); + for (int i = 0; i <= template; i++) { + out.add(lines[i]); + } + out.addAll(body); + for (int i = bodyEnd; i < lines.length; i++) { + out.add(lines[i]); + } + return String.join("\n", out); + } + + /** The dump without the routeTemplates of the given ids: what is left are the routes of the other files. */ + static String withoutTemplates(String dump, Set<String> ids) { + if (ids.isEmpty()) { + return dump; + } + List<String> out = new ArrayList<>(); + List<String> item = new ArrayList<>(); + boolean drop = false; + for (String l : dump.split("\n", -1)) { + if (l.startsWith("- ")) { + if (!drop) { + out.addAll(item); + } + item = new ArrayList<>(); + drop = false; + } + item.add(l); + Matcher m = TEMPLATE_ID.matcher(l); + if (!item.isEmpty() && item.get(0).startsWith("- routeTemplate:") && m.find() && ids.contains(m.group(1))) { + drop = true; + } + } + if (!drop) { + out.addAll(item); + } + return String.join("\n", out).strip(); + } + + /** The lines of the routeTemplate with the given id in the dump, or null. */ + static List<String> templateChunk(String dump, String templateId) { + if (dump == null) { + return null; + } + List<String> current = null; + boolean match = false; + for (String l : dump.split("\n", -1)) { + if (l.startsWith("- routeTemplate:")) { + if (match) { + return current; + } + current = new ArrayList<>(); + match = false; + } + if (current != null) { + current.add(l); + Matcher m = TEMPLATE_ID.matcher(l); + if (m.find() && m.group(1).equals(templateId)) { + match = true; + } + } + } + return match ? current : null; + } + + /** The key line at the indent and the lines under it, or null when the key is not there. */ + static List<String> block(List<String> lines, String key, int indent) { + List<String> answer = null; + for (String l : lines) { + if (answer == null) { + if (indent(l) == indent && l.trim().equals(key)) { + answer = new ArrayList<>(); + answer.add(l); + } + } else if (l.isBlank() || indent(l) > indent) { + answer.add(l); + } else { + break; + } + } + if (answer != null) { + while (!answer.isEmpty() && answer.get(answer.size() - 1).isBlank()) { + answer.remove(answer.size() - 1); + } + } + return answer; + } + + private static List<String> shift(List<String> lines, int by) { + List<String> answer = new ArrayList<>(); + for (String l : lines) { + if (l.isBlank()) { + answer.add(""); + } else if (by >= 0) { + answer.add(" ".repeat(by) + l); + } else { + answer.add(l.substring(Math.min(-by, indent(l)))); + } + } + return answer; + } + + private static int indent(String line) { + int i = 0; + while (i < line.length() && line.charAt(i) == ' ') { + i++; + } + return i; + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeCommand.java b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeCommand.java index 38fb2a0c80e0..3b54f4ef1e2e 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeCommand.java +++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeCommand.java @@ -20,8 +20,12 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Set; import java.util.Stack; import org.apache.camel.dsl.jbang.core.commands.CamelCommand; @@ -71,6 +75,14 @@ public class YamlNormalizeCommand extends CamelCommand { if (matched.isEmpty()) { return 0; } + // a Kamelet loads as a route template: its template is normalized and put back into the Kamelet file + Map<String, String> kamelets = new LinkedHashMap<>(); + for (String n : matched) { + String content = readFile(n); + if (content != null && KameletNormalizer.isKamelet(content)) { + kamelets.put(n, content); + } + } String dump = CommandLineHelper.CAMEL_JBANG_WORK_DIR + "/normalize-output.yaml"; Files.deleteIfExists(Path.of(dump)); @@ -80,7 +92,8 @@ public class YamlNormalizeCommand extends CamelCommand { @Override protected void doAddInitialProperty(KameletMain main) { main.addInitialProperty("camel.main.dumpRoutes", "yaml"); - main.addInitialProperty("camel.main.dumpRoutesInclude", "routes,rests,routeConfigurations,beans,dataFormats"); + main.addInitialProperty("camel.main.dumpRoutesInclude", + "routes,rests,routeConfigurations,routeTemplates,beans,dataFormats"); main.addInitialProperty("camel.main.dumpRoutesLog", "false"); main.addInitialProperty("camel.main.dumpRoutesResolvePlaceholders", "false"); main.addInitialProperty("camel.main.dumpRoutesUriAsParameters", "true"); @@ -101,24 +114,67 @@ public class YamlNormalizeCommand extends CamelCommand { String normalized = waitForDumpFile(Path.of(target)); if (normalized == null) { - printer().printErr("Error normalizing files"); + printer().printErr("Error normalizing files: nothing was loaded from " + String.join(", ", matched) + + " (see the errors above)"); return 1; } + // the dump has one item per routeTemplate, route, rest...: those of the Kamelets go back into their files + Map<String, String> kameletOutput = new LinkedHashMap<>(); + Set<String> kameletIds = new HashSet<>(); + for (Map.Entry<String, String> e : kamelets.entrySet()) { + String id = KameletNormalizer.kameletName(e.getValue(), e.getKey()); + String k = KameletNormalizer.normalize(e.getValue(), normalized, id); + if (k == null) { + printer().printErr("Error normalizing the Kamelet " + e.getKey() + ": its template " + id + + " did not load (see the errors above), or the file has no spec.template"); + return 1; + } + kameletIds.add(id); + kameletOutput.put(e.getKey(), k); + } + String routes = KameletNormalizer.withoutTemplates(normalized, kameletIds); + + List<String> documents = new ArrayList<>(); + if (!routes.isBlank()) { + documents.add(routes); + } + documents.addAll(kameletOutput.values()); if (output != null) { Path outPath = Path.of(output); if (Files.isDirectory(outPath)) { - outPath = outPath.resolve("normalized.yaml"); + if (!routes.isBlank()) { + Files.writeString(outPath.resolve("normalized.yaml"), routes); + } + for (Map.Entry<String, String> e : kameletOutput.entrySet()) { + Files.writeString(outPath.resolve(Path.of(e.getKey()).getFileName()), e.getValue()); + } + } else if (documents.size() > 1 && !kameletOutput.isEmpty()) { + printer().printErr("--output must be a directory to normalize Kamelets with other files:" + + " each Kamelet is written to a file of its own name"); + return 1; + } else { + Files.writeString(outPath, documents.get(0)); } - Files.writeString(outPath, normalized); printer().println("Normalized " + matched.size() + " file(s) to " + output); } else { - printer().println(normalized); + // more than one document: one YAML stream, separated by --- + printer().println(String.join("\n---\n", documents)); } return 0; } + private static String readFile(String name) { + try { + String n = name.startsWith("file:") ? name.substring(5) : name; + Path p = Path.of(n); + return Files.isRegularFile(p) ? Files.readString(p) : null; + } catch (Exception e) { + return null; + } + } + private String waitForDumpFile(Path dumpFile) { StopWatch watch = new StopWatch(); while (watch.taken() < 5000) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeKameletTest.java b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeKameletTest.java new file mode 100644 index 000000000000..c106d232db15 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeKameletTest.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.dsl.jbang.core.commands.validate; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; + +import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import org.apache.camel.dsl.jbang.core.common.StringPrinter; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import picocli.CommandLine; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * CAMEL-25381: camel validate normalize on a Kamelet file normalizes its template and keeps the rest of the file. + */ +class YamlNormalizeKameletTest { + + private static final Path KAMELET = Path.of("src/test/resources/kamelets/tag-order-action.kamelet.yaml"); + private static final Path ROUTE = Path.of("src/test/resources/route.yaml"); + + private static StringPrinter printer; + + private static int normalize(String... args) throws Exception { + printer = new StringPrinter(); + YamlNormalizeCommand cmd = new YamlNormalizeCommand(new CamelJBangMain().withPrinter(printer)); + CommandLine.populateCommand(cmd, args); + return cmd.doCall(); + } + + @Test + void aKameletIsNormalizedInItsTemplateOnly(@TempDir Path out) throws Exception { + assertThat(normalize("--output=" + out, KAMELET.toString())).isZero(); + String original = Files.readString(KAMELET); + String normalized = Files.readString(out.resolve("tag-order-action.kamelet.yaml")); + + // the license, metadata and definition (with its comment) as written, and what follows the template + assertThat(normalized).startsWith(original.substring(0, original.indexOf(" template:\n") + 12)); + assertThat(normalized).endsWith(original.substring(original.indexOf(" dependencies:"))); + // the template in canonical form: expressions under expression:, endpoints as uri: + String template = normalized.substring(normalized.indexOf(" template:\n"), normalized.indexOf(" dependencies:")); + assertThat(template.replaceAll("\\s+", " ")) + .contains("- setBody: expression: simple: expression: \"${body} [{{tag}}]\"") + .contains("- expression: simple: expression: \"${header.priority} == true\""); + assertThat(template).contains("uri: kamelet:source").contains("uri: kamelet:sink") + .doesNotContain("templateId"); + // the bean properties in the order they were written + assertThat(template.indexOf("zeta:")).isLessThan(template.indexOf("alpha:")); + } + + @Test + void aKameletIsPrintedWhenThereIsNoOutput() throws Exception { + assertThat(normalize(KAMELET.toString())).isZero(); + assertThat(printer.getOutput()).contains("kind: Kamelet").contains("name: tag-order-action") + .doesNotContain("routeTemplate"); + } + + @Test + void aKameletAndARouteAreTwoDocuments() throws Exception { + assertThat(normalize(KAMELET.toString(), ROUTE.toString())).isZero(); + String output = printer.getOutput(); + assertThat(output).contains("\n---\n").contains("kind: Kamelet").contains("- route:") + .doesNotContain("routeTemplate"); + } + + @Test + void theTemplateIsReplacedInTheKameletText() { + String kamelet = """ + apiVersion: camel.apache.org/v1 + kind: Kamelet + metadata: + name: my-action + spec: + definition: + title: Mine + template: + from: + uri: kamelet:source + steps: + - setBody: + constant: hi + + types: + out: + mediaType: text/plain + """; + String dump = """ + - routeTemplate: + id: other + route: + from: + uri: timer + - routeTemplate: + id: my-action + parameters: + - name: x + route: + from: + uri: kamelet:source + steps: + - setBody: + expression: + constant: + expression: hi + - route: + from: + uri: direct + """; + String normalized = KameletNormalizer.normalize(kamelet, dump, "my-action"); + assertThat(normalized).isEqualTo(""" + apiVersion: camel.apache.org/v1 + kind: Kamelet + metadata: + name: my-action + spec: + definition: + title: Mine + template: + from: + uri: kamelet:source + steps: + - setBody: + expression: + constant: + expression: hi + + types: + out: + mediaType: text/plain + """); + assertThat(KameletNormalizer.withoutTemplates(dump, Set.of("my-action"))) + .contains("id: other").contains("- route:").doesNotContain("my-action"); + assertThat(KameletNormalizer.normalize(kamelet, dump, "missing")).isNull(); + } + + @Test + void theNameOfAKamelet() { + assertThat(KameletNormalizer.kameletName("kind: Kamelet\nmetadata:\n name: abc\nspec:\n", "x.kamelet.yaml")) + .isEqualTo("abc"); + assertThat(KameletNormalizer.kameletName("kind: Kamelet\nspec:\n", "dir/xyz.kamelet.yaml")).isEqualTo("xyz"); + assertThat(KameletNormalizer.isKamelet("apiVersion: camel.apache.org/v1\nkind: Kamelet\n")).isTrue(); + assertThat(KameletNormalizer.isKamelet("- route:\n from:\n")).isFalse(); + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/kamelets/tag-order-action.kamelet.yaml b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/kamelets/tag-order-action.kamelet.yaml new file mode 100644 index 000000000000..ee57b86262fd --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/kamelets/tag-order-action.kamelet.yaml @@ -0,0 +1,62 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +apiVersion: camel.apache.org/v1 +kind: Kamelet +metadata: + name: tag-order-action + labels: + camel.apache.org/kamelet.type: action +spec: + definition: + title: Tag Order + # the tag is what the agent passes + required: + - tag + type: object + properties: + tag: + title: Tag + type: string + priority: + title: Priority + type: boolean + default: false + template: + beans: + - name: tagger + type: "#class:java.util.HashMap" + properties: + zeta: "{{tag}}" + alpha: "{{priority}}" + from: + uri: kamelet:source + steps: + - choice: + when: + - simple: "${header.priority} == true" + steps: + - setBody: + simple: "${body} [!{{tag}}]" + otherwise: + steps: + - setBody: + simple: "${body} [{{tag}}]" + - to: kamelet:sink + dependencies: + - "camel:core" + - "camel:kamelet" diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerSupport.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerSupport.java index 4b2bef9d6b5d..b2baa35c1c7e 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerSupport.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerSupport.java @@ -20,8 +20,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -179,7 +179,7 @@ public class YamlDeserializerSupport { } final MappingNode mn = asMappingNode(node); - final Map<String, Object> answer = new HashMap<>(); + final Map<String, Object> answer = new LinkedHashMap<>(); for (NodeTuple tuple : mn.getValue()) { final String key = asText(tuple.getKeyNode()); @@ -206,7 +206,7 @@ public class YamlDeserializerSupport { } final MappingNode mn = asMappingNode(node); - final Map<String, Object> answer = new HashMap<>(); + final Map<String, Object> answer = new LinkedHashMap<>(); for (NodeTuple tuple : mn.getValue()) { final String key = asText(tuple.getKeyNode()); diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/PipeLoaderErrorHandlerTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/PipeLoaderErrorHandlerTest.java index a11cd9f8ad01..ce9530549e32 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/PipeLoaderErrorHandlerTest.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/PipeLoaderErrorHandlerTest.java @@ -81,8 +81,9 @@ class PipeLoaderErrorHandlerTest extends YamlTestSupport { assertThat(route.getErrorHandlerFactory()).isNotNull(); assertThat(route.getErrorHandlerFactory()).isInstanceOf(DeadLetterChannelDefinition.class); DeadLetterChannelDefinition eh = (DeadLetterChannelDefinition) route.getErrorHandlerFactory(); + // the properties in the order the Pipe declares them assertThat(eh.getDeadLetterUri()).isEqualTo( - "kamelet:error-handler?kafkaTopic=my-first-test&logMessage=ERROR!&kafkaServiceAccountId=scott&kafkaBrokers=my-broker&kafkaServiceAccountSecret=tiger"); + "kamelet:error-handler?logMessage=ERROR!&kafkaBrokers=my-broker&kafkaTopic=my-first-test&kafkaServiceAccountId=scott&kafkaServiceAccountSecret=tiger"); assertThat(eh.getRedeliveryPolicy().getMaximumRedeliveries()).isEqualTo("1"); assertThat(eh.getRedeliveryPolicy().getRedeliveryDelay()).isEqualTo("2000"); assertThat(route.getRouteId()).isEqualTo("timer-event-source");
