From 4a6c2e02b2d1e61e4181908917236993375626ae Mon Sep 17 00:00:00 2001 From: Luca Burgazzoli Date: Fri, 22 Jan 2021 15:12:59 +0100 Subject: [PATCH] loader(java): support for jshell scripts --- .../camel/k/loader/java/JavaSourceLoader.java | 4 + .../org/apache/camel/k/loader/jsh/Jsh.java | 119 ++++++++++++++++++ .../camel/k/loader/jsh/JshSourceLoader.java | 104 +++++++++++++++ .../k/loader/jsh/JshSourceLoaderTest.groovy | 45 +++++++ .../impl/src/test/resources/jsh/routes.jsh | 20 +++ .../impl/src/test/resources/log4j2-test.xml | 1 + docs/modules/languages/nav-languages.adoc | 1 + docs/modules/languages/pages/jsh.adoc | 25 ++++ docs/modules/languages/pages/languages.adoc | 1 + .../maven/processors/CatalogProcessor3x.java | 2 +- 10 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/jsh/Jsh.java create mode 100644 camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/jsh/JshSourceLoader.java create mode 100644 camel-k-loader-java/impl/src/test/groovy/org/apache/camel/k/loader/jsh/JshSourceLoaderTest.groovy create mode 100644 camel-k-loader-java/impl/src/test/resources/jsh/routes.jsh create mode 100644 docs/modules/languages/pages/jsh.adoc diff --git a/camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/java/JavaSourceLoader.java b/camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/java/JavaSourceLoader.java index b3e7f8de0..5f15f91a0 100644 --- a/camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/java/JavaSourceLoader.java +++ b/camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/java/JavaSourceLoader.java @@ -33,6 +33,10 @@ import org.apache.camel.util.IOHelper; import org.joor.Reflect; +/** + * A {@link SourceLoader} implementation based on jOOR (https://github.com/jOOQ/jOOR) + * to compile Java source file at runtime. + */ @Loader(value = "java") public class JavaSourceLoader implements SourceLoader { private static final Pattern PACKAGE_PATTERN = Pattern.compile("^\\s*package\\s+([a-zA-Z][\\.\\w]*)\\s*;.*$", Pattern.MULTILINE); diff --git a/camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/jsh/Jsh.java b/camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/jsh/Jsh.java new file mode 100644 index 000000000..f4b8f7c78 --- /dev/null +++ b/camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/jsh/Jsh.java @@ -0,0 +1,119 @@ +/* + * 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.k.loader.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 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> BINDINGS = ThreadLocal.withInitial(ConcurrentHashMap::new); + + private Jsh() { + // no-op + } + + public static List compile(JShell jshell, String script) throws ScriptException { + List 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 void setBinding(JShell jshell, String name, T value, Class 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 getBinding(String name, Class 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 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()); + } + } + } +} diff --git a/camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/jsh/JshSourceLoader.java b/camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/jsh/JshSourceLoader.java new file mode 100644 index 000000000..8d5fb0963 --- /dev/null +++ b/camel-k-loader-java/impl/src/main/java/org/apache/camel/k/loader/jsh/JshSourceLoader.java @@ -0,0 +1,104 @@ +/* + * 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.k.loader.jsh; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import javax.script.ScriptException; + +import jdk.jshell.JShell; +import jdk.jshell.execution.DirectExecutionControl; +import jdk.jshell.spi.ExecutionControl; +import jdk.jshell.spi.ExecutionControlProvider; +import jdk.jshell.spi.ExecutionEnv; +import org.apache.camel.CamelContext; +import org.apache.camel.Experimental; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.endpoint.EndpointRouteBuilder; +import org.apache.camel.k.Source; +import org.apache.camel.k.SourceLoader; +import org.apache.camel.k.annotation.Loader; +import org.apache.camel.k.support.RouteBuilders; +import org.apache.camel.spi.Registry; +import org.apache.camel.util.IOHelper; + +/** + * A {@link SourceLoader} implementation based on {@link JShell}. + */ +@Experimental +@Loader(value = "jsh") +public class JshSourceLoader implements SourceLoader { + @Override + public Collection getSupportedLanguages() { + return Collections.singletonList("jsh"); + } + + @Override + public RoutesBuilder load(CamelContext camelContext, Source source) { + return RouteBuilders.endpoint( + source, + (reader, builder) -> { + final String content = IOHelper.toString(reader); + final ExecutionControlProvider provider = new ExecutionControlProvider() { + private final ExecutionControl control = new DirectExecutionControl(); + + @Override + public String name() { + return "jsh-direct"; + } + + @Override + public ExecutionControl generate(ExecutionEnv env, Map parameters) throws Throwable { + return control; + } + }; + + // + // Leverage DirectExecutionControl as execution engine to make JShell running + // in the current process and give a chance to bind variables to the script. + // + 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(); + } + } + ); + } +} diff --git a/camel-k-loader-java/impl/src/test/groovy/org/apache/camel/k/loader/jsh/JshSourceLoaderTest.groovy b/camel-k-loader-java/impl/src/test/groovy/org/apache/camel/k/loader/jsh/JshSourceLoaderTest.groovy new file mode 100644 index 000000000..0ccc59296 --- /dev/null +++ b/camel-k-loader-java/impl/src/test/groovy/org/apache/camel/k/loader/jsh/JshSourceLoaderTest.groovy @@ -0,0 +1,45 @@ +/* + * 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.k.loader.jsh + + +import org.apache.camel.k.loader.java.support.TestRuntime +import org.apache.camel.model.ProcessDefinition +import org.apache.camel.model.ToDefinition +import spock.lang.AutoCleanup +import spock.lang.Specification + +class JshSourceLoaderTest extends Specification { + @AutoCleanup + def runtime = new TestRuntime() + + def "load"(location) { + expect: + runtime.loadRoutes(location) + + with(runtime.context.routeDefinitions) { + it[0].input.endpointUri ==~ /timer:.*tick/ + it[0].outputs[0] instanceof ProcessDefinition + it[0].outputs[1] instanceof ToDefinition + } + where: + location << [ + "classpath:jsh/routes.jsh" + ] + + } +} diff --git a/camel-k-loader-java/impl/src/test/resources/jsh/routes.jsh b/camel-k-loader-java/impl/src/test/resources/jsh/routes.jsh new file mode 100644 index 000000000..99bfe4f50 --- /dev/null +++ b/camel-k-loader-java/impl/src/test/resources/jsh/routes.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/camel-k-loader-java/impl/src/test/resources/log4j2-test.xml b/camel-k-loader-java/impl/src/test/resources/log4j2-test.xml index e2e9f4226..f2a5fb238 100644 --- a/camel-k-loader-java/impl/src/test/resources/log4j2-test.xml +++ b/camel-k-loader-java/impl/src/test/resources/log4j2-test.xml @@ -26,6 +26,7 @@ + diff --git a/docs/modules/languages/nav-languages.adoc b/docs/modules/languages/nav-languages.adoc index 6fde60dfd..abeab4171 100644 --- a/docs/modules/languages/nav-languages.adoc +++ b/docs/modules/languages/nav-languages.adoc @@ -3,5 +3,6 @@ ** xref:languages:kotlin.adoc[Kotlin] ** xref:languages:javascript.adoc[JavaScript] ** xref:languages:java.adoc[Java] +** xref:languages:jsh.adoc[Java (JShell)] ** xref:languages:xml.adoc[XML] ** xref:languages:yaml.adoc[YAML] diff --git a/docs/modules/languages/pages/jsh.adoc b/docs/modules/languages/pages/jsh.adoc new file mode 100644 index 000000000..5adc8f35c --- /dev/null +++ b/docs/modules/languages/pages/jsh.adoc @@ -0,0 +1,25 @@ += Writing Integrations in Java + +[CAUTION] +==== +Support for JShell is highly experimental +==== + +Using Java and JShell to write an integration to be deployed using Camel K is no different from defining your routing rules in Camel with the only difference that you do not need to implement or extend a RouteBuilder but you can access the current builder thx to the built-in `builder` variable. + +[source,java] +.example.jsh +---- +builder.from("timer:tick") + .setBody() + .constant("Hello Camel K!") + .to("log:info");- +---- + +You can run it with the standard command: + +``` +kamel run example.jsh +``` + + diff --git a/docs/modules/languages/pages/languages.adoc b/docs/modules/languages/pages/languages.adoc index afd395ac4..e4fa85a95 100644 --- a/docs/modules/languages/pages/languages.adoc +++ b/docs/modules/languages/pages/languages.adoc @@ -9,6 +9,7 @@ Camel K supports multiple languages for writing integrations: |======================= | Language | Description | xref:java.adoc[Java] | Integrations written in Java DSL are supported. +| xref:jsh.adoc[JShell] | Integrations written in Java DSL using JShell. | xref:xml.adoc[XML] | Integrations written in plain XML DSL are supported (Spring XML with or Blueprint XML with not supported). | xref:yaml.adoc[YAML] | Integrations written in YAML DSL are supported. | xref:groovy.adoc[Groovy] | Groovy `.groovy` files are supported (experimental). diff --git a/support/camel-k-maven-plugin/src/main/java/org/apache/camel/k/tooling/maven/processors/CatalogProcessor3x.java b/support/camel-k-maven-plugin/src/main/java/org/apache/camel/k/tooling/maven/processors/CatalogProcessor3x.java index bb239a2f1..1bcb83d46 100644 --- a/support/camel-k-maven-plugin/src/main/java/org/apache/camel/k/tooling/maven/processors/CatalogProcessor3x.java +++ b/support/camel-k-maven-plugin/src/main/java/org/apache/camel/k/tooling/maven/processors/CatalogProcessor3x.java @@ -216,7 +216,7 @@ private static void processLoaders(CamelCatalogSpec.Builder specBuilder) { specBuilder.putLoader( "java", CamelLoader.fromArtifact("org.apache.camel.k", "camel-k-loader-java") - .addLanguage("java") + .addLanguages("java", "jsh") .putMetadata("native", "false") .build() );