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 54836b991f5c CAMEL-25079: camel-yaml-io, camel-java-io - Route dumpers 
produce invalid YAML and Java (#26970)
54836b991f5c is described below

commit 54836b991f5cc5fa59080b5fe7fb9399a033c4a7
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 29 09:44:44 2026 +0200

    CAMEL-25079: camel-yaml-io, camel-java-io - Route dumpers produce invalid 
YAML and Java (#26970)
    
    CAMEL-25079: camel-yaml-io, camel-java-io - Route dumpers produce invalid 
YAML and Java: fix bugs found in a deep review
    
    - Java dump of route templates, rests, route configurations and rest 
configuration uses the Java DSL writers
    - Java string literals escape line breaks
    - YAML quotes a trailing colon, a leading , ] }, carriage returns, and text 
that yaml reads as a number;
      a multi-line value that a block scalar cannot keep is written 
double-quoted
    - YAML only writes a value as a number when it is written as that number 
(007 stays text)
    - dumpDataFormatsAsYaml writes a list of data formats (as the YAML DSL 
loads them)
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../org/apache/camel/java/LwModelToJavaDumper.java | 14 ++--
 .../camel/java/out/JavaDslModelWriterSupport.java  |  5 +-
 .../apache/camel/java/LwModelToJavaDumperTest.java | 76 ++++++++++++++++++++++
 .../org/apache/camel/yaml/LwModelToYAMLDumper.java | 31 +++++----
 .../java/org/apache/camel/yaml/io/YamlPrinter.java | 25 +++++--
 .../camel/yaml/out/YamlModelWriterSupport.java     | 11 +++-
 .../camel/yaml/io/YamlPrinterRoundTripTest.java    | 47 +++++++++++++
 .../camel/yaml/out/YamlWriterEdgeCasesTest.java    | 69 ++++++++++++++++++++
 8 files changed, 248 insertions(+), 30 deletions(-)

diff --git 
a/core/camel-java-io/src/main/java/org/apache/camel/java/LwModelToJavaDumper.java
 
b/core/camel-java-io/src/main/java/org/apache/camel/java/LwModelToJavaDumper.java
index 2ca544b1c848..a31ffe45207e 100644
--- 
a/core/camel-java-io/src/main/java/org/apache/camel/java/LwModelToJavaDumper.java
+++ 
b/core/camel-java-io/src/main/java/org/apache/camel/java/LwModelToJavaDumper.java
@@ -107,30 +107,30 @@ public class LwModelToJavaDumper implements 
ModelToJavaDumper {
                     if (!sb.isEmpty()) {
                         sb.append("\n\n");
                     }
-                    sb.append(writer.writeRouteTemplateDefinition(template));
+                    sb.append(writer.writeRouteTemplate(template));
                 }
             } else if (definition instanceof RouteTemplateDefinition template) 
{
-                sb.append(writer.writeRouteTemplateDefinition(template));
+                sb.append(writer.writeRouteTemplate(template));
             } else if (definition instanceof RestsDefinition rd) {
                 for (RestDefinition rest : rd.getRests()) {
                     if (!sb.isEmpty()) {
                         sb.append("\n\n");
                     }
-                    sb.append(writer.writeRestDefinition(rest));
+                    sb.append(writer.writeRest(rest));
                 }
             } else if (definition instanceof RestDefinition rest) {
-                sb.append(writer.writeRestDefinition(rest));
+                sb.append(writer.writeRest(rest));
             } else if (definition instanceof RouteConfigurationsDefinition 
rcd) {
                 for (RouteConfigurationDefinition config : 
rcd.getRouteConfigurations()) {
                     if (!sb.isEmpty()) {
                         sb.append("\n\n");
                     }
-                    
sb.append(writer.writeRouteConfigurationDefinition(config));
+                    sb.append(writer.writeRouteConfiguration(config));
                 }
             } else if (definition instanceof RouteConfigurationDefinition 
config) {
-                sb.append(writer.writeRouteConfigurationDefinition(config));
+                sb.append(writer.writeRouteConfiguration(config));
             } else if (definition instanceof RestConfigurationDefinition 
restConfig) {
-                sb.append(writer.writeRestConfigurationDefinition(restConfig));
+                sb.append(writer.writeRestConfiguration(restConfig));
             } else if (definition instanceof TransformersDefinition td) {
                 for (TransformerDefinition t : td.getTransformers()) {
                     if (!sb.isEmpty()) {
diff --git 
a/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java
 
b/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java
index 466c471060d5..f54e1dcd2aa5 100644
--- 
a/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java
+++ 
b/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java
@@ -1135,7 +1135,10 @@ public abstract class JavaDslModelWriterSupport {
         if (s == null) {
             return "null";
         }
-        return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
+        // escape as a java string literal (a line break is not allowed in a 
string literal)
+        return "\"" + s.replace("\\", "\\\\").replace("\"", 
"\\\"").replace("\n", "\\n").replace("\r", "\\r")
+                .replace("\t", "\\t")
+               + "\"";
     }
 
     protected String classLiteral(String typeName) {
diff --git 
a/core/camel-java-io/src/test/java/org/apache/camel/java/LwModelToJavaDumperTest.java
 
b/core/camel-java-io/src/test/java/org/apache/camel/java/LwModelToJavaDumperTest.java
new file mode 100644
index 000000000000..f040d2bd772a
--- /dev/null
+++ 
b/core/camel-java-io/src/test/java/org/apache/camel/java/LwModelToJavaDumperTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.java;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.builder.RouteConfigurationBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.model.Model;
+import org.apache.camel.model.RouteConfigurationDefinition;
+import org.apache.camel.model.RouteTemplateDefinition;
+import org.apache.camel.model.rest.RestDefinition;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class LwModelToJavaDumperTest {
+
+    @Test
+    public void testDumpRouteTemplateRestAndRouteConfiguration() throws 
Exception {
+        try (DefaultCamelContext context = new DefaultCamelContext()) {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    
routeTemplate("tpl").templateParameter("name").from("direct:{{name}}").to("mock:result");
+
+                    rest("/api").get("/hello").to("direct:hello");
+                }
+            });
+            context.addRoutes(new RouteConfigurationBuilder() {
+                @Override
+                public void configuration() {
+                    
routeConfiguration("cfg").onException(IllegalArgumentException.class).handled(true);
+                }
+            });
+            Model model = 
context.getCamelContextExtension().getContextPlugin(Model.class);
+            LwModelToJavaDumper dumper = new LwModelToJavaDumper();
+
+            RouteTemplateDefinition template = 
model.getRouteTemplateDefinition("tpl");
+            assertThat(dumper.dumpModelAsJava(context, 
template)).startsWith("routeTemplate(\"tpl\")");
+
+            RestDefinition rest = model.getRestDefinitions().get(0);
+            assertThat(dumper.dumpModelAsJava(context, 
rest)).startsWith("rest(\"/api\")");
+
+            RouteConfigurationDefinition config = 
model.getRouteConfigurationDefinitions().get(0);
+            assertThat(dumper.dumpModelAsJava(context, 
config)).startsWith("routeConfiguration(\"cfg\")");
+        }
+    }
+
+    @Test
+    public void testDumpMultiLineExpression() throws Exception {
+        try (DefaultCamelContext context = new DefaultCamelContext()) {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    
from("direct:start").routeId("myRoute").setBody(simple("Hello\n  
${body}")).to("mock:result");
+                }
+            });
+            String java = new LwModelToJavaDumper().dumpModelAsJava(context, 
context.getRouteDefinition("myRoute"));
+            assertThat(java).contains("\"Hello\\n  ${body}\"");
+        }
+    }
+}
diff --git 
a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java
 
b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java
index 2a3ea94848be..9d4f14b3e024 100644
--- 
a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java
+++ 
b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java
@@ -54,6 +54,7 @@ import org.apache.camel.spi.ModelToYAMLDumper;
 import org.apache.camel.spi.NamespaceAware;
 import org.apache.camel.spi.annotations.JdkService;
 import org.apache.camel.util.KeyValueHolder;
+import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.yaml.out.YamlModelWriter;
 
@@ -461,22 +462,24 @@ public class LwModelToYAMLDumper implements 
ModelToYAMLDumper {
                 return;
             }
 
-            buffer.write("- dataFormats:\n");
-
-            DataFormatsDefinition def = new DataFormatsDefinition();
-            def.setDataFormats(new ArrayList<>(dataFormats.values()));
-
-            YamlModelWriter writer = new YamlModelWriter();
+            // a list of data formats, each as its own single-key map (such as 
- json: ...), as the yaml dsl loads them
+            // (and so two data formats of the same kind do not overwrite each 
other)
+            var writer = new YamlModelWriter() {
+                @Override
+                public JsonObject 
doWriteDataFormatsDefinition(DataFormatsDefinition def) {
+                    return super.doWriteDataFormatsDefinition(def);
+                }
+            };
             writer.setCamelContext(camelContext);
-            JsonObject jo = writer.writeDataFormatsDefinition(def);
-            List<JsonObject> roots = new ArrayList<>();
-            roots.add(jo);
-            String yaml = writer.printAsYaml(roots);
-            for (String line : yaml.split("\n")) {
-                buffer.write("    ");
-                buffer.write(line);
-                buffer.write("\n");
+            JsonArray list = new JsonArray();
+            for (DataFormatDefinition df : dataFormats.values()) {
+                DataFormatsDefinition def = new DataFormatsDefinition();
+                def.setDataFormats(List.of(df));
+                list.add(writer.doWriteDataFormatsDefinition(def));
             }
+            JsonObject root = new JsonObject();
+            root.put("dataFormats", list);
+            buffer.write(writer.printAsYaml(List.of(root)));
         }
     }
 
diff --git 
a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/YamlPrinter.java 
b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/YamlPrinter.java
index d573d031aaa3..812e27782e90 100644
--- a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/YamlPrinter.java
+++ b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/YamlPrinter.java
@@ -28,7 +28,9 @@ import java.util.regex.Pattern;
 public final class YamlPrinter {
 
     private static final String INDENT = "  ";
-    private static final Pattern NUMBER_PATTERN = 
Pattern.compile("-?(0|[1-9]\\d*)(\\.\\d+)?([eE][+-]?\\d+)?");
+    // what a yaml parser reads as a number (yaml 1.2 core schema, such as 
007, +5, 1., .5, .inf, 0x1f)
+    private static final Pattern NUMBER_PATTERN = Pattern.compile(
+            
"[-+]?(\\.[0-9]+|[0-9]+(\\.[0-9]*)?)([eE][-+]?[0-9]+)?|0o[0-7]+|0x[0-9a-fA-F]+|[-+]?\\.(inf|Inf|INF)|\\.(nan|NaN|NAN)");
 
     private YamlPrinter() {
     }
@@ -89,7 +91,7 @@ public final class YamlPrinter {
                 writeSequenceItems(sb, col, indent + 1, false);
             } else {
                 sb.append(' ');
-                if (value instanceof String s && s.contains("\n")) {
+                if (value instanceof String s && s.contains("\n") && 
canBeBlockScalar(s)) {
                     writeBlockScalar(sb, s, indent + 1);
                 } else {
                     writeScalar(sb, value);
@@ -99,6 +101,15 @@ public final class YamlPrinter {
         }
     }
 
+    /**
+     * Whether the value can be written as a literal block scalar (|- or |), 
otherwise it is written double-quoted. A
+     * block scalar cannot keep more than one trailing line break (without 
|+), a first line that starts with a space
+     * (without an indentation indicator), or a carriage return.
+     */
+    private static boolean canBeBlockScalar(String value) {
+        return !value.endsWith("\n\n") && !value.startsWith(" ") && 
!value.startsWith("\t") && !value.contains("\r");
+    }
+
     private static void writeBlockScalar(StringBuilder sb, String value, int 
indent) {
         if (value.endsWith("\n")) {
             sb.append("|\n");
@@ -126,7 +137,8 @@ public final class YamlPrinter {
             String s = String.valueOf(value);
             if (needsQuoting(s)) {
                 sb.append('"');
-                sb.append(s.replace("\\", "\\\\").replace("\"", "\\\""));
+                sb.append(s.replace("\\", "\\\\").replace("\"", 
"\\\"").replace("\n", "\\n").replace("\r", "\\r")
+                        .replace("\t", "\\t"));
                 sb.append('"');
             } else {
                 sb.append(s);
@@ -143,11 +155,12 @@ public final class YamlPrinter {
         if (first == ' ' || first == '\t' || first == '-' || first == '?' || 
first == '*'
                 || first == '&' || first == '!' || first == '%' || first == 
'@' || first == '`'
                 || first == '\'' || first == '"' || first == '{' || first == 
'[' || first == '>'
-                || first == '|' || first == '#' || first == '$') {
+                || first == '|' || first == '#' || first == '$' || first == 
',' || first == ']' || first == '}') {
             return true;
         }
 
-        if (s.charAt(s.length() - 1) == ' ' || s.charAt(s.length() - 1) == 
'\t') {
+        char last = s.charAt(s.length() - 1);
+        if (last == ' ' || last == '\t' || last == ':') {
             return true;
         }
 
@@ -165,7 +178,7 @@ public final class YamlPrinter {
             if (c == '{' && i + 1 < s.length() && s.charAt(i + 1) == '{') {
                 return true;
             }
-            if (c == '\n') {
+            if (c == '\n' || c == '\r') {
                 return true;
             }
         }
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 3c024975df00..4fe49a127205 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
@@ -217,13 +217,20 @@ public abstract class YamlModelWriterSupport {
         if ("true".equals(value) || "false".equals(value)) {
             return Boolean.parseBoolean(value);
         }
+        // only a number when it is written as the number (such as 007, +5 or 
1e3 are text, which is quoted)
         try {
-            return Long.parseLong(value);
+            long l = Long.parseLong(value);
+            if (Long.toString(l).equals(value)) {
+                return l;
+            }
         } catch (NumberFormatException e) {
             // not a long
         }
         try {
-            return Double.parseDouble(value);
+            double d = Double.parseDouble(value);
+            if (Double.toString(d).equals(value)) {
+                return d;
+            }
         } catch (NumberFormatException e) {
             // not a double
         }
diff --git 
a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/io/YamlPrinterRoundTripTest.java
 
b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/io/YamlPrinterRoundTripTest.java
new file mode 100644
index 000000000000..0290567b8c6d
--- /dev/null
+++ 
b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/io/YamlPrinterRoundTripTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.yaml.io;
+
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class YamlPrinterRoundTripTest {
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "Processing:", ",leading comma", "]x", "}x", "a\rb", " x\ny", "  
lead\nnext", "x\n\n", "007", "+5", "1.",
+            ".inf", ".nan", "1e3", "0x1f" })
+    public void testStringValue(String value) throws Exception {
+        JsonObject log = new JsonObject();
+        log.put("message", value);
+        JsonObject root = new JsonObject(Map.of("log", log));
+
+        String yaml = YamlPrinter.print(List.of(root));
+        JsonNode node = new YAMLMapper().readTree(yaml);
+        JsonNode message = node.get(0).get("log").get("message");
+        assertThat(message.isTextual()).as(yaml).isTrue();
+        assertThat(message.asText()).as(yaml).isEqualTo(value);
+    }
+}
diff --git 
a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlWriterEdgeCasesTest.java
 
b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlWriterEdgeCasesTest.java
new file mode 100644
index 000000000000..7ce72aa8a0eb
--- /dev/null
+++ 
b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlWriterEdgeCasesTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.yaml.out;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.dataformat.JsonDataFormat;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.yaml.LwModelToYAMLDumper;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class YamlWriterEdgeCasesTest {
+
+    @Test
+    public void testTextThatLooksLikeNumber() throws Exception {
+        RouteDefinition route = new RouteDefinition();
+        route.from("direct:a").setHeader("007").constant("x").log("1e3");
+
+        YamlModelWriter writer = new YamlModelWriter();
+        JsonObject jo = writer.writeRouteDefinition(route);
+        String yaml = writer.printAsYaml(List.of(jo));
+
+        JsonNode node = new YAMLMapper().readTree(yaml);
+        JsonNode steps = node.get(0).get("route").get("from").get("steps");
+        
assertThat(steps.get(0).get("setHeader").get("name").asText()).as(yaml).isEqualTo("007");
+        
assertThat(steps.get(1).get("log").get("message").asText()).as(yaml).isEqualTo("1e3");
+    }
+
+    @Test
+    public void testDumpDataFormats() throws Exception {
+        JsonDataFormat json1 = new JsonDataFormat();
+        json1.setId("one");
+        JsonDataFormat json2 = new JsonDataFormat();
+        json2.setId("two");
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("one", json1);
+        map.put("two", json2);
+
+        String yaml = new LwModelToYAMLDumper().dumpDataFormatsAsYaml(null, 
map);
+        JsonNode node = new YAMLMapper().readTree(yaml);
+        // a list of data formats, each as its own single-key map
+        JsonNode list = node.get(0).get("dataFormats");
+        assertThat(list.isArray()).as(yaml).isTrue();
+        assertThat(list.size()).as(yaml).isEqualTo(2);
+        
assertThat(list.get(0).get("json").get("id").asText()).isEqualTo("one");
+        
assertThat(list.get(1).get("json").get("id").asText()).isEqualTo("two");
+    }
+}

Reply via email to