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 d65b06d9f962 CAMEL-24851: when a YAML route file does not load, camel 
run logs the schema validator's report before the loader's error (#26636)
d65b06d9f962 is described below

commit d65b06d9f962bb27f9f68dcfd0676a054fe5e819
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Sep 20 21:07:50 2026 +0200

    CAMEL-24851: when a YAML route file does not load, camel run logs the 
schema validator's report before the loader's error (#26636)
    
    * CAMEL-24851: when a YAML route file does not load, camel run logs the 
schema validator's report (what to write) before the loader's error, on a 
failed start and on a failed reload in dev mode
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
    
    * CAMEL-24851: the upgrade note is migration-only and does not split the 
runtime paragraphs, the running docs show the report, a Windows path is not a 
scheme, no duplicate files, and swallowed exceptions are logged at debug
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
    
    ---------
    
    Co-authored-by: Claude Fable 5.1 <[email protected]>
---
 .../support/FileWatcherResourceReloadStrategy.java |   2 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   5 +
 .../modules/ROOT/pages/camel-jbang-running.adoc    |  11 ++
 .../apache/camel/dsl/jbang/core/commands/Run.java  |  13 ++
 .../core/commands/ai/YamlLoadFailureReport.java    | 198 +++++++++++++++++++++
 .../commands/ai/YamlLoadFailureReportTest.java     | 108 +++++++++++
 .../java/org/apache/camel/main/KameletMain.java    |  19 ++
 7 files changed, 356 insertions(+)

diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
index 59b38793aa7a..c0f3e9da8c4a 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
@@ -314,6 +314,8 @@ public class FileWatcherResourceReloadStrategy extends 
ResourceReloadStrategySup
                             } catch (Exception e) {
                                 setLastError(e);
                                 incFailedCounter();
+                                // the same event a failed context reload 
emits, so a listener can act on the file
+                                
EventHelper.notifyContextReloadFailure(getCamelContext(), name, e);
                                 String msg = e.getMessage();
                                 if (msg.endsWith(".")) {
                                     msg = msg.substring(0, msg.length() - 1);
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 2f627f9065a9..4bef77ec5004 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -786,6 +786,11 @@ Running an existing Maven project (`camel run pom.xml`) is 
unchanged, and still
 from the `pom.xml`. The other commands that take `--runtime` (such as `camel 
export`,
 `camel dependency list` and `camel version list`) accept `jbang` as an alias 
for `main`.
 
+The file watcher reload strategy (camel-support) now emits the 
`CamelContextReloadFailure` event when a single
+file fails to reload, with the file name as the event's action; before, only a 
failed reload of the whole context
+emitted it. A listener of that event fires for these failures too. `camel run` 
uses it to print the report of
+`camel validate yaml` when a YAML route file does not load, at start and on a 
reload in `--dev` mode.
+
 The YAML that `camel init` writes (`camel init foo.yaml`, and the Integration 
and Kamelet templates), the
 bundled examples (`camel init --example`) and the routes the 
`camel_ai_pipeline_scaffold` and
 `camel_openapi_scaffold` MCP tools generate are now written in the canonical 
YAML DSL format, with an
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
index faf644de04ca..9af930c01171 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
@@ -20,6 +20,17 @@ This works for all DSLs (YAML, Java, XML).
 NOTE: Live reload is for development purposes. If you encounter JVM class 
loading issues,
 restart the integration. Java files are not live-reloadable in Spring Boot 
runtime.
 
+When a YAML route file does not load, at start or after a save in dev mode, 
`camel run` prints the report of
+`camel validate yaml` for the file before the loader's error, so the message 
says what to write:
+
+[source,text]
+----
+The route file did not load. camel validate yaml says what to write:
+  orders.camel.yaml:
+    pollEnrich: property 'uri' is not defined in the schema ... (the endpoint 
of pollEnrich is an expression: write pollEnrich: {expression: {constant: 
{expression: "file:./order.json"}}})
+  (camel validate yaml <file> for the full report; the loader's error follows)
+----
+
 === Source directory
 
 Use `--source-dir` for more flexibility — Camel watches the entire directory 
(including subfolders)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
index 37da002093fe..4ba4f527fe11 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
@@ -47,6 +47,7 @@ import jdk.jfr.Configuration;
 import jdk.jfr.Recording;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.YamlLoadFailureReport;
 import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
 import org.apache.camel.dsl.jbang.core.common.EnvironmentHelper;
 import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
@@ -73,6 +74,7 @@ import 
org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 import org.apache.camel.main.BaseMainSupport;
 import org.apache.camel.main.KameletMain;
+import org.apache.camel.main.MainListenerSupport;
 import org.apache.camel.main.download.DownloadListener;
 import org.apache.camel.main.util.SuggestSimilarHelper;
 import org.apache.camel.spi.BacklogDebugger;
@@ -1421,6 +1423,17 @@ public class Run extends CamelCommand {
         } else {
             // run default in current JVM with same camel version
             try {
+                // a YAML route file that did not load: the validator's report 
(what to write) before the loader's
+                // error, on a failed start and on a failed reload in dev mode 
(CAMEL-24851)
+                final List<String> runFiles = new ArrayList<>(files);
+                main.setStartFailureListener(ex -> 
YamlLoadFailureReport.logIfYamlLoadFailure(ex, runFiles));
+                main.addMainListener(new MainListenerSupport() {
+                    @Override
+                    public void afterConfigure(BaseMainSupport main) {
+                        main.getCamelContext().getManagementStrategy()
+                                .addEventNotifier(new 
YamlLoadFailureReport.ReloadFailureNotifier());
+                    }
+                });
                 return runKameletMain(main);
             } catch (Exception ex) {
                 if (ignoreLoadingError) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReport.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReport.java
new file mode 100644
index 000000000000..f7a8c96bb53b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReport.java
@@ -0,0 +1,198 @@
+/*
+ * 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.ai;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
+import org.apache.camel.impl.event.CamelContextReloadFailureEvent;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.support.SimpleEventNotifierSupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * When a YAML route file fails to load, the loader's message says which node 
is wrong but not what to write; the schema
+ * validator has the message that does, with its hints. This runs the 
validator on the route files of a failed start (or
+ * of a failed reload in dev mode) and prints its report, so the hints reach 
everyone who runs, whether they validated
+ * first or not (CAMEL-24851).
+ */
+public final class YamlLoadFailureReport {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(YamlLoadFailureReport.class);
+
+    private static volatile CamelCatalog catalog;
+
+    private YamlLoadFailureReport() {
+    }
+
+    /** Whether the failure comes from loading a YAML route file: a 
deserialization error or a pre-parse error. */
+    public static boolean isYamlLoadFailure(Throwable failure) {
+        for (Throwable t = failure; t != null; t = t.getCause() == t ? null : 
t.getCause()) {
+            if (t instanceof YamlDeserializationException) {
+                return true;
+            }
+            String msg = t.getMessage();
+            if (msg != null && (msg.startsWith("Error pre-parsing resource") 
|| msg.contains("Error constructing YAML node")
+                    || msg.contains("Error parsing YAML"))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Logs the validator's report for the YAML files of a run whose start 
failed on loading one of them; nothing for
+     * any other failure. Logged (not printed) so that it is in the console 
and in the run's log file alike, where the
+     * camel-jbang-mcp tools read it, and before the loader's own error.
+     */
+    public static void logIfYamlLoadFailure(Throwable failure, List<String> 
files) {
+        if (!isYamlLoadFailure(failure)) {
+            return;
+        }
+        List<String> lines = report(yamlFiles(files));
+        if (!lines.isEmpty()) {
+            LOG.error(String.join(System.lineSeparator(), lines));
+        }
+    }
+
+    /**
+     * The validator's report for the YAML files, as lines to print: a header, 
then per file with problems its name and
+     * the messages, then a footer; empty when the validator finds nothing 
(then the loader's message is all there is).
+     */
+    public static List<String> report(List<Path> yamlFiles) {
+        List<String> lines = new ArrayList<>();
+        for (Path file : yamlFiles) {
+            if (!Files.isRegularFile(file)) {
+                continue;
+            }
+            List<String> errors;
+            try {
+                String content = Files.readString(file);
+                Path dir = file.toAbsolutePath().getParent();
+                errors = 
SourceValidator.validate(file.getFileName().toString(), content, catalog(), 
null, dir);
+            } catch (Exception e) {
+                // the validator must never hide the loader's error
+                LOG.debug("Cannot validate {}", file, e);
+                continue;
+            }
+            if (!errors.isEmpty()) {
+                if (lines.isEmpty()) {
+                    lines.add("The route file did not load. camel validate 
yaml says what to write:");
+                }
+                lines.add("  " + file.getFileName() + ":");
+                for (String error : errors) {
+                    lines.add("    " + error);
+                }
+            }
+        }
+        if (!lines.isEmpty()) {
+            lines.add("  (camel validate yaml <file> for the full report; the 
loader's error follows)");
+        }
+        return lines;
+    }
+
+    /** The YAML route files among the files of a run: local files ending in 
.yaml or .yml. */
+    public static List<Path> yamlFiles(List<String> files) {
+        List<Path> answer = new ArrayList<>();
+        if (files == null) {
+            return answer;
+        }
+        for (String f : files) {
+            if (f == null) {
+                continue;
+            }
+            if (f.startsWith("file:")) {
+                f = f.substring(5);
+            }
+            if (ResourceHelper.hasScheme(f) || f.startsWith("github:")) {
+                continue; // github:, https:, classpath: (the check Run makes; 
a Windows drive letter is not a scheme)
+            }
+            String lower = f.toLowerCase();
+            Path path = Path.of(f);
+            if ((lower.endsWith(".yaml") || lower.endsWith(".yml")) && 
Files.isRegularFile(path) && !answer.contains(path)) {
+                answer.add(path);
+            }
+        }
+        return answer;
+    }
+
+    private static CamelCatalog catalog() {
+        CamelCatalog answer = catalog;
+        if (answer == null) {
+            synchronized (YamlLoadFailureReport.class) {
+                if (catalog == null) {
+                    catalog = new DefaultCamelCatalog();
+                }
+                answer = catalog;
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Prints the report for a route file whose reload failed in dev mode: the 
file watcher emits the reload failure
+     * event with the file name as the action.
+     */
+    public static final class ReloadFailureNotifier extends 
SimpleEventNotifierSupport {
+
+        private final Consumer<String> printer;
+
+        /** Logs the report, as {@link #logIfYamlLoadFailure(Throwable, List)} 
does. */
+        public ReloadFailureNotifier() {
+            this(null);
+        }
+
+        public ReloadFailureNotifier(Consumer<String> printer) {
+            this.printer = printer;
+            setIgnoreCamelContextEvents(false);
+            setIgnoreExchangeEvents(true);
+            // the reload events are dispatched under the route events flag: 
it must stay off
+            setIgnoreRouteEvents(false);
+            setIgnoreServiceEvents(true);
+        }
+
+        @Override
+        public boolean isEnabled(CamelEvent event) {
+            return event instanceof CamelEvent.CamelContextReloadFailureEvent;
+        }
+
+        @Override
+        public void notify(CamelEvent event) {
+            // the file watcher emits the event with the file name as the 
action (the API's getSource() is the context)
+            if (event instanceof CamelContextReloadFailureEvent failure
+                    && failure.getAction() instanceof String name && 
isYamlLoadFailure(failure.getCause())) {
+                List<String> lines = report(yamlFiles(List.of(name)));
+                if (lines.isEmpty()) {
+                    return;
+                }
+                if (printer != null) {
+                    lines.forEach(printer);
+                } else {
+                    LOG.error(String.join(System.lineSeparator(), lines));
+                }
+            }
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReportTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReportTest.java
new file mode 100644
index 000000000000..a06a76854a49
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLoadFailureReportTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.ai;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
+import org.apache.camel.impl.engine.SimpleCamelContext;
+import org.apache.camel.impl.event.CamelContextReloadFailureEvent;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** CAMEL-24851: a YAML file that did not load gets the validator's report, 
which says what to write. */
+class YamlLoadFailureReportTest {
+
+    @TempDir
+    Path dir;
+
+    private static final String BAD_ROUTE = """
+            - route:
+                from:
+                  uri: timer:tick
+                  steps:
+                    - pollEnrich:
+                        uri: file:./order.json
+                    - to:
+                        uri: log:done
+            """;
+
+    @Test
+    void aDeserializationErrorIsAYamlLoadFailure() {
+        Exception loader
+                = new YamlDeserializationException("Error constructing YAML 
node id: pollEnrich: unsupported field: uri");
+        assertThat(YamlLoadFailureReport.isYamlLoadFailure(new 
RuntimeCamelException("Error starting Camel", loader))).isTrue();
+        assertThat(
+                YamlLoadFailureReport.isYamlLoadFailure(new 
RuntimeCamelException("Error pre-parsing resource: file:x.yaml")))
+                .isTrue();
+        assertThat(YamlLoadFailureReport.isYamlLoadFailure(new 
IllegalArgumentException("Invalid directory: archived/${x}")))
+                .isFalse();
+    }
+
+    @Test
+    void theReportSaysWhatToWrite() throws Exception {
+        Path route = dir.resolve("orders.camel.yaml");
+        Files.writeString(route, BAD_ROUTE);
+        List<String> lines = YamlLoadFailureReport.report(List.of(route));
+        assertThat(lines).hasSize(4);
+        assertThat(lines.get(0)).isEqualTo("The route file did not load. camel 
validate yaml says what to write:");
+        assertThat(lines.get(1)).isEqualTo("  orders.camel.yaml:");
+        // the schema message (with the CAMEL-24850 hint once it is in: "write 
pollEnrich: {expression: ...}")
+        assertThat(lines.get(2)).startsWith("    
").contains("pollEnrich").contains("'uri'");
+        assertThat(lines.get(3)).startsWith("  (camel validate yaml <file> for 
the full report");
+    }
+
+    @Test
+    void aFileTheValidatorAcceptsGivesNoReport() throws Exception {
+        Path route = dir.resolve("ok.camel.yaml");
+        Files.writeString(route, BAD_ROUTE.replace("uri: file:./order.json",
+                "expression:\n              constant:\n                
expression: \"file:./order.json\""));
+        assertThat(YamlLoadFailureReport.report(List.of(route))).isEmpty();
+    }
+
+    @Test
+    void theRunFilesAreFilteredToLocalYaml() throws Exception {
+        Path route = dir.resolve("a.yaml");
+        Files.writeString(route, BAD_ROUTE);
+        List<Path> files
+                = YamlLoadFailureReport.yamlFiles(List.of("file:" + route, 
route.toString(), dir.resolve("Foo.java").toString(),
+                        "github:apache:camel:x.yaml", 
dir.resolve("missing.yaml").toString()));
+        assertThat(files).containsExactly(route);
+    }
+
+    @Test
+    void theReloadNotifierPrintsTheReportForTheFileThatFailed() throws 
Exception {
+        Path route = dir.resolve("orders.camel.yaml");
+        Files.writeString(route, BAD_ROUTE);
+        List<String> printed = new ArrayList<>();
+        YamlLoadFailureReport.ReloadFailureNotifier notifier = new 
YamlLoadFailureReport.ReloadFailureNotifier(printed::add);
+        // the event the file watcher emits: the file name is the action, the 
loader's exception the cause
+        CamelContextReloadFailureEvent event = new 
CamelContextReloadFailureEvent(
+                new SimpleCamelContext(), route.toString(),
+                new YamlDeserializationException("Error constructing YAML node 
id: pollEnrich: unsupported field: uri"));
+        assertThat(notifier.isEnabled(event)).isTrue();
+        notifier.notify(event);
+        assertThat(printed).hasSize(4);
+        assertThat(printed.get(2)).contains("pollEnrich");
+    }
+}
diff --git 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java
index 0da255df653a..8be7dadd91e6 100644
--- 
a/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java
+++ 
b/dsl/camel-kamelet-main/src/main/java/org/apache/camel/main/KameletMain.java
@@ -26,6 +26,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.TreeMap;
+import java.util.function.Consumer;
 import java.util.stream.Stream;
 
 import org.w3c.dom.Document;
@@ -166,6 +167,8 @@ public class KameletMain extends MainCommandLineSupport {
         configureInitialProperties(locations);
     }
 
+    private Consumer<Exception> startFailureListener;
+
     public static void main(String... args) throws Exception {
         KameletMain main = new KameletMain();
         int code = main.run(args);
@@ -174,8 +177,24 @@ public class KameletMain extends MainCommandLineSupport {
         System.exit(code);
     }
 
+    /**
+     * Called with the exception when Camel fails to start, before the error 
is logged: the CLI uses it to report what
+     * the schema validator says about a YAML route file that did not load 
(CAMEL-24851).
+     */
+    public void setStartFailureListener(Consumer<Exception> 
startFailureListener) {
+        this.startFailureListener = startFailureListener;
+    }
+
     @Override
     protected void doFail(Exception e) {
+        if (startFailureListener != null) {
+            try {
+                startFailureListener.accept(e);
+            } catch (Exception listenerFailure) {
+                // the listener must not hide the failure
+                LOG.debug("Start failure listener failed: {}", 
listenerFailure.getMessage(), listenerFailure);
+            }
+        }
         // ensure any unhandled fatal errors are also logged before 
terminating process
         LOG.error("Error starting Camel: {}", e, e);
         super.doFail(e);

Reply via email to