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");
+ }
+}