You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2021/12/25 15:06:49 UTC

[camel] 01/02: CAMEL-17382: camel-jsh-dsl - JavaShell DSL support

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

commit f417456dd0981c96dafe83976f73eadb506a89e9
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Sat Dec 25 16:03:30 2021 +0100

    CAMEL-17382: camel-jsh-dsl - JavaShell DSL support
---
 dsl/camel-jsh-dsl/pom.xml                          | 145 +++++++++++++++++++++
 .../services/org/apache/camel/routes-loader/jsh    |   2 +
 dsl/camel-jsh-dsl/src/main/docs/jsh-dsl.adoc       |  12 ++
 .../main/java/org/apache/camel/dsl/jsh/Jsh.java    | 135 +++++++++++++++++++
 .../org/apache/camel/dsl/jsh/JshClassLoader.java   |  52 ++++++++
 .../apache/camel/dsl/jsh/JshLoaderDelegate.java    |  80 ++++++++++++
 .../camel/dsl/jsh/JshRoutesBuilderLoader.java      | 112 ++++++++++++++++
 .../apache/camel/dsl/jsh/JshSourceLoaderTest.java  |  60 +++++++++
 .../src/test/resources/log4j2-test.properties      |  31 +++++
 .../src/test/resources/routes/MyRoute.jsh          |  20 +++
 dsl/pom.xml                                        |   1 +
 parent/pom.xml                                     |   5 +
 12 files changed, 655 insertions(+)

diff --git a/dsl/camel-jsh-dsl/pom.xml b/dsl/camel-jsh-dsl/pom.xml
new file mode 100644
index 0000000..861ddda
--- /dev/null
+++ b/dsl/camel-jsh-dsl/pom.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.camel</groupId>
+        <artifactId>dsl</artifactId>
+        <version>3.15.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>camel-jsh-dsl</artifactId>
+    <packaging>jar</packaging>
+    <name>Camel :: JavaShell DSL</name>
+    <description>Camel DSL with JavaShell</description>
+
+    <properties>
+        <firstVersion>3.15.0</firstVersion>
+        <supportLevel>Experimental</supportLevel>
+        <sourcecheckExcludes>
+            **/resources/**/My*.java
+        </sourcecheckExcludes>
+        <sourcecheckExcludesComma>
+            ${sourcecheckExcludes},
+        </sourcecheckExcludesComma>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-endpointdsl-support</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-main</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-direct</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-rest</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-mock</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-core-languages</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-bean</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-log</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-telegram</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-seda</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-test-junit5</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-slf4j-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-jcl</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.camel</groupId>
+                <artifactId>camel-package-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>generate-spi</id>
+                        <goals>
+                            <goal>generate-spi</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
\ No newline at end of file
diff --git a/dsl/camel-jsh-dsl/src/generated/resources/META-INF/services/org/apache/camel/routes-loader/jsh b/dsl/camel-jsh-dsl/src/generated/resources/META-INF/services/org/apache/camel/routes-loader/jsh
new file mode 100644
index 0000000..f8cf758
--- /dev/null
+++ b/dsl/camel-jsh-dsl/src/generated/resources/META-INF/services/org/apache/camel/routes-loader/jsh
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.dsl.jsh.JshRoutesBuilderLoader
diff --git a/dsl/camel-jsh-dsl/src/main/docs/jsh-dsl.adoc b/dsl/camel-jsh-dsl/src/main/docs/jsh-dsl.adoc
new file mode 100644
index 0000000..d71a63b
--- /dev/null
+++ b/dsl/camel-jsh-dsl/src/main/docs/jsh-dsl.adoc
@@ -0,0 +1,12 @@
+= JavaShell DSL Component
+//TODO there is no .json file for this doc page, so it is not updated automatically by UpdateReadmeMojo.
+//Header attributes written by hand.
+:doctitle: JavaShell DSL
+:artifactid: camel-jsh-dsl
+:description: Camel DSL with JavaShell
+:supportlevel: Experimental/Preview
+:since: 3.15
+//Manually maintained attributes
+:group: DSL
+
+See xref:manual:ROOT:dsl.adoc[DSL]
diff --git a/dsl/camel-jsh-dsl/src/main/java/org/apache/camel/dsl/jsh/Jsh.java b/dsl/camel-jsh-dsl/src/main/java/org/apache/camel/dsl/jsh/Jsh.java
new file mode 100644
index 0000000..8b29d71
--- /dev/null
+++ b/dsl/camel-jsh-dsl/src/main/java/org/apache/camel/dsl/jsh/Jsh.java
@@ -0,0 +1,135 @@
+/*
+ * 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.jsh;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.script.ScriptException;
+
+import jdk.jshell.JShell;
+import jdk.jshell.Snippet;
+import jdk.jshell.SnippetEvent;
+import jdk.jshell.SourceCodeAnalysis;
+import jdk.jshell.spi.ExecutionControl;
+import jdk.jshell.spi.ExecutionControlProvider;
+import jdk.jshell.spi.ExecutionEnv;
+import org.apache.camel.util.ObjectHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class Jsh {
+    private static final Logger LOGGER = LoggerFactory.getLogger(Jsh.class);
+    private static final ThreadLocal<Map<String, Object>> BINDINGS = ThreadLocal.withInitial(ConcurrentHashMap::new);
+
+    private Jsh() {
+        // no-op
+    }
+
+    public static List<String> compile(JShell jshell, String script) throws ScriptException {
+        List<String> snippets = new ArrayList<>();
+
+        while (!script.isEmpty()) {
+            SourceCodeAnalysis.CompletionInfo ci = jshell.sourceCodeAnalysis().analyzeCompletion(script);
+            if (!ci.completeness().isComplete()) {
+                throw new ScriptException("Incomplete script:\n" + script);
+            }
+
+            snippets.add(ci.source());
+            script = ci.remaining();
+        }
+
+        return snippets;
+    }
+
+    public static void setBinding(JShell jshell, String name, Object value) throws ScriptException {
+        ObjectHelper.notNull(jshell, "jshell");
+        ObjectHelper.notNull(name, "name");
+        ObjectHelper.notNull(value, "value");
+
+        setBinding(jshell, name, value, value.getClass());
+    }
+
+    public static <T> void setBinding(JShell jshell, String name, T value, Class<? extends T> type) throws ScriptException {
+        ObjectHelper.notNull(jshell, "jshell");
+        ObjectHelper.notNull(name, "name");
+        ObjectHelper.notNull(value, "value");
+        ObjectHelper.notNull(type, "type");
+
+        setBinding(name, value);
+
+        // As JShell leverages LocalExecutionControl as execution engine and thus JShell
+        // runs in the current process it is possible to access to local classes, we use
+        // such capability to inject bindings as variables.
+        String snippet = String.format(
+                "var %s = %s.getBinding(\"%s\", %s.class);",
+                name,
+                Jsh.class.getName(),
+                name,
+                type.getName());
+
+        eval(jshell, snippet);
+    }
+
+    public static Object getBinding(String name) {
+        return BINDINGS.get().get(name);
+    }
+
+    public static <T> T getBinding(String name, Class<T> type) {
+        Object answer = BINDINGS.get().get(name);
+        return answer != null ? type.cast(answer) : null;
+    }
+
+    public static void setBinding(String name, Object value) {
+        BINDINGS.get().put(name, value);
+    }
+
+    public static void clearBindings() {
+        BINDINGS.get().clear();
+    }
+
+    public static void eval(JShell jshell, String snippet) throws ScriptException {
+        LOGGER.debug("Evaluating {}", snippet);
+
+        List<SnippetEvent> events = jshell.eval(snippet);
+
+        for (SnippetEvent event : events) {
+            if (event.exception() != null) {
+                throw new ScriptException(event.exception());
+            }
+            if (event.status() != Snippet.Status.VALID) {
+                throw new ScriptException("Error evaluating snippet:\n" + event.snippet().source());
+            }
+        }
+    }
+
+    public static ExecutionControlProvider wrapExecutionControl(String name, ExecutionControl delegate) {
+        return new ExecutionControlProvider() {
+            @Override
+            public String name() {
+                return name;
+            }
+
+            @Override
+            public ExecutionControl generate(ExecutionEnv env, Map<String, String> parameters) throws Throwable {
+                return delegate;
+            }
+        };
+    }
+}
diff --git a/dsl/camel-jsh-dsl/src/main/java/org/apache/camel/dsl/jsh/JshClassLoader.java b/dsl/camel-jsh-dsl/src/main/java/org/apache/camel/dsl/jsh/JshClassLoader.java
new file mode 100644
index 0000000..530ba8f
--- /dev/null
+++ b/dsl/camel-jsh-dsl/src/main/java/org/apache/camel/dsl/jsh/JshClassLoader.java
@@ -0,0 +1,52 @@
+/*
+ * 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.jsh;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import jdk.jshell.spi.ExecutionControl;
+
+/**
+ * An implementation of a {@link ClassLoader} that allow hold class bytecode.
+ */
+final class JshClassLoader extends ClassLoader {
+    private final Map<String, ExecutionControl.ClassBytecodes> types;
+
+    JshClassLoader(ClassLoader parent) {
+        super(parent);
+        this.types = new HashMap<>();
+    }
+
+    void addClassBytecodes(ExecutionControl.ClassBytecodes classBytecodes) {
+        types.put(toResourceString(classBytecodes.name()), classBytecodes);
+    }
+
+    @Override
+    protected Class<?> findClass(String name) throws ClassNotFoundException {
+        final String key = toResourceString(name);
+        final ExecutionControl.ClassBytecodes cb = types.get(key);
+
+        return cb == null
+                ? super.findClass(name)
+                : super.defineClass(name, cb.bytecodes(), 0, cb.bytecodes().length);
+    }
+
+    private static String toResourceString(String name) {
+        return name.replace('.', '/') + ".class";
+    }
+}
diff --git a/dsl/camel-jsh-dsl/src/main/java/org/apache/camel/dsl/jsh/JshLoaderDelegate.java b/dsl/camel-jsh-dsl/src/main/java/org/apache/camel/dsl/jsh/JshLoaderDelegate.java
new file mode 100644
index 0000000..e50009d
--- /dev/null
+++ b/dsl/camel-jsh-dsl/src/main/java/org/apache/camel/dsl/jsh/JshLoaderDelegate.java
@@ -0,0 +1,80 @@
+/*
+ * 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.jsh;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import jdk.jshell.execution.LoaderDelegate;
+import jdk.jshell.spi.ExecutionControl;
+
+/**
+ * A simple implementation of {@link LoaderDelegate} tailored for camel-k use case.
+ */
+final class JshLoaderDelegate implements LoaderDelegate {
+    private final JshClassLoader loader;
+    private final Map<String, Class<?>> types;
+
+    public JshLoaderDelegate(JshClassLoader loader) {
+        this.loader = loader;
+        this.types = new HashMap<>();
+    }
+
+    @SuppressWarnings("PMD.PreserveStackTrace")
+    @Override
+    public void load(ExecutionControl.ClassBytecodes[] cbs)
+            throws ExecutionControl.ClassInstallException, ExecutionControl.EngineTerminationException {
+
+        boolean[] loaded = new boolean[cbs.length];
+        try {
+            for (ExecutionControl.ClassBytecodes cb : cbs) {
+                loader.addClassBytecodes(cb);
+            }
+            for (int i = 0; i < cbs.length; ++i) {
+                Class<?> type = loader.loadClass(cbs[i].name());
+                type.getDeclaredMethods();
+
+                types.put(cbs[i].name(), type);
+
+                loaded[i] = true;
+            }
+        } catch (Throwable ex) {
+            throw new ExecutionControl.ClassInstallException("load: " + ex.getMessage(), loaded);
+        }
+    }
+
+    @Override
+    public void classesRedefined(ExecutionControl.ClassBytecodes[] cbcs) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public void addToClasspath(String cp)
+            throws ExecutionControl.EngineTerminationException, ExecutionControl.InternalException {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    @Override
+    public Class<?> findClass(String name) throws ClassNotFoundException {
+        Class<?> type = types.get(name);
+        if (type != null) {
+            return type;
+        }
+
+        throw new ClassNotFoundException(name + " not found");
+    }
+}
diff --git a/dsl/camel-jsh-dsl/src/main/java/org/apache/camel/dsl/jsh/JshRoutesBuilderLoader.java b/dsl/camel-jsh-dsl/src/main/java/org/apache/camel/dsl/jsh/JshRoutesBuilderLoader.java
new file mode 100644
index 0000000..7e21669
--- /dev/null
+++ b/dsl/camel-jsh-dsl/src/main/java/org/apache/camel/dsl/jsh/JshRoutesBuilderLoader.java
@@ -0,0 +1,112 @@
+/*
+ * 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.jsh;
+
+import java.io.Reader;
+
+import javax.script.ScriptException;
+
+import jdk.jshell.JShell;
+import jdk.jshell.execution.DirectExecutionControl;
+import jdk.jshell.execution.LoaderDelegate;
+import jdk.jshell.spi.ExecutionControl;
+import jdk.jshell.spi.ExecutionControlProvider;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Experimental;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.api.management.ManagedAttribute;
+import org.apache.camel.api.management.ManagedResource;
+import org.apache.camel.builder.endpoint.EndpointRouteBuilder;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.spi.Resource;
+import org.apache.camel.spi.RoutesBuilderLoader;
+import org.apache.camel.spi.annotations.RoutesLoader;
+import org.apache.camel.support.RoutesBuilderLoaderSupport;
+import org.apache.camel.util.IOHelper;
+
+/**
+ * A {@link RoutesBuilderLoader} implementation based on {@link JShell}.
+ */
+@ManagedResource(description = "Managed JShell RoutesBuilderLoader")
+@Experimental
+@RoutesLoader("jsh")
+public class JshRoutesBuilderLoader extends RoutesBuilderLoaderSupport {
+    public static final String EXTENSION = "jsh";
+
+    @ManagedAttribute(description = "Supported file extension")
+    @Override
+    public String getSupportedExtension() {
+        return EXTENSION;
+    }
+
+    @Override
+    public RoutesBuilder loadRoutesBuilder(Resource resource) throws Exception {
+        return EndpointRouteBuilder.loadEndpointRoutesBuilder(resource, JshRoutesBuilderLoader::eval);
+    }
+
+    private static void eval(Reader reader, EndpointRouteBuilder builder) throws Exception {
+        final ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+        final String content = IOHelper.toString(reader);
+
+        //
+        // By default the jdk.jshell.execution.DefaultLoaderDelegate uses a
+        // custom URL class-loader and does not provide any option to set the
+        // parent which causes the ThreadLocal hack used to inject bindings
+        // to fail as there are two copies fo the Jsh class (one from the
+        // Quarkus class loader and one for the custom one).
+        //
+        final JshClassLoader jshcl = new JshClassLoader(tccl);
+        final LoaderDelegate delegate = new JshLoaderDelegate(jshcl);
+        final ExecutionControl control = new DirectExecutionControl(delegate);
+        final ExecutionControlProvider provider = Jsh.wrapExecutionControl("jsh-direct", control);
+
+        Thread.currentThread().setContextClassLoader(jshcl);
+
+        //
+        // Leverage DirectExecutionControl as execution engine to make JShell running
+        // in the current process and give a chance to bind variables to the script
+        // using ThreadLocal hack.
+        //
+        try (JShell jshell = JShell.builder().executionEngine(provider, null).build()) {
+            //
+            // since we can't set a base class for the snippet as we do for other
+            // languages (groovy, kotlin) we need to introduce a top level variable
+            // that users need to use to access the RouteBuilder, like:
+            //
+            //     builder.from("timer:tick")
+            //         .to("log:info")
+            //
+            // context and thus registry can easily be retrieved from the registered
+            // variable `builder` but for a better UX, add them as top level vars.
+            //
+            Jsh.setBinding(jshell, "builder", builder, EndpointRouteBuilder.class);
+            Jsh.setBinding(jshell, "context", builder.getContext(), CamelContext.class);
+            Jsh.setBinding(jshell, "registry", builder.getContext().getRegistry(), Registry.class);
+
+            for (String snippet : Jsh.compile(jshell, content)) {
+                Jsh.eval(jshell, snippet);
+            }
+        } catch (ScriptException e) {
+            throw new RuntimeException(e);
+        } finally {
+            // remove contextual bindings once the snippet has been evaluated
+            Jsh.clearBindings();
+            // restore original TCCL
+            Thread.currentThread().setContextClassLoader(tccl);
+        }
+    }
+}
diff --git a/dsl/camel-jsh-dsl/src/test/java/org/apache/camel/dsl/jsh/JshSourceLoaderTest.java b/dsl/camel-jsh-dsl/src/test/java/org/apache/camel/dsl/jsh/JshSourceLoaderTest.java
new file mode 100644
index 0000000..e10859f
--- /dev/null
+++ b/dsl/camel-jsh-dsl/src/test/java/org/apache/camel/dsl/jsh/JshSourceLoaderTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.jsh;
+
+import java.util.Collection;
+
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.model.ProcessorDefinition;
+import org.apache.camel.model.ToDefinition;
+import org.apache.camel.spi.Resource;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class JshSourceLoaderTest {
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+            "/routes/MyRoute.jsh"
+    })
+    void testLoadRoutes(String location) throws Exception {
+        try (DefaultCamelContext context = new DefaultCamelContext()) {
+            Resource resource = context.getResourceLoader().resolveResource(location);
+            Collection<RoutesBuilder> builders = context.getRoutesLoader().findRoutesBuilders(resource);
+
+            assertThat(builders).hasSize(1);
+
+            RouteBuilder builder = (RouteBuilder) builders.iterator().next();
+            builder.setContext(context);
+            builder.configure();
+
+            Assertions.assertThat(builder.getRouteCollection().getRoutes())
+                    .hasSize(1)
+                    .first()
+                    .satisfies(rd -> {
+                        Assertions.assertThat(rd.getInput().getEndpointUri()).matches("timer:.*tick");
+                        Assertions.assertThat(rd.getOutputs().get(0)).isInstanceOf(ProcessorDefinition.class);
+                        Assertions.assertThat(rd.getOutputs().get(1)).isInstanceOf(ToDefinition.class);
+                    });
+        }
+    }
+}
diff --git a/dsl/camel-jsh-dsl/src/test/resources/log4j2-test.properties b/dsl/camel-jsh-dsl/src/test/resources/log4j2-test.properties
new file mode 100644
index 0000000..55f5c41
--- /dev/null
+++ b/dsl/camel-jsh-dsl/src/test/resources/log4j2-test.properties
@@ -0,0 +1,31 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+
+appender.file.type = File
+appender.file.name = file
+appender.file.fileName = target/camel-jsh-dsl-test.log
+appender.file.layout.type = PatternLayout
+appender.file.layout.pattern = %d [%-15.15t] %-5p %-30.30c{1} - %m%n
+
+appender.out.type = Console
+appender.out.name = out
+appender.out.layout.type = PatternLayout
+appender.out.layout.pattern = [%30.30t] %-30.30c{1} %-5p %m%n
+
+rootLogger.level = INFO
+rootLogger.appenderRef.file.ref = file
+#rootLogger.appenderRef.out.ref = out
diff --git a/dsl/camel-jsh-dsl/src/test/resources/routes/MyRoute.jsh b/dsl/camel-jsh-dsl/src/test/resources/routes/MyRoute.jsh
new file mode 100644
index 0000000..99bfe4f
--- /dev/null
+++ b/dsl/camel-jsh-dsl/src/test/resources/routes/MyRoute.jsh
@@ -0,0 +1,20 @@
+/*
+ * 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.
+ */
+
+builder.from("timer:tick")
+    .process(e -> {})
+    .to("log:info");
\ No newline at end of file
diff --git a/dsl/pom.xml b/dsl/pom.xml
index 592aa6e..4a1224d 100644
--- a/dsl/pom.xml
+++ b/dsl/pom.xml
@@ -45,6 +45,7 @@
         <module>camel-xml-jaxb-dsl-test</module>
         <module>camel-yaml-dsl</module>
         <module>camel-js-dsl</module>
+        <module>camel-jsh-dsl</module>
         <module>camel-kotlin-dsl</module>
         <module>camel-kamelet-main</module>
         <module>camel-jbang</module>
diff --git a/parent/pom.xml b/parent/pom.xml
index 0ae244a..dfdadc0 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -2809,6 +2809,11 @@
 				<artifactId>camel-js-dsl</artifactId>
 				<version>${project.version}</version>
 			</dependency>
+            <dependency>
+                <groupId>org.apache.camel</groupId>
+                <artifactId>camel-jsh-dsl</artifactId>
+                <version>${project.version}</version>
+            </dependency>
 			<dependency>
 				<groupId>org.apache.camel</groupId>
 				<artifactId>camel-kotlin-dsl</artifactId>